brindle 0.3.0__tar.gz → 0.3.1__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 (124) hide show
  1. {brindle-0.3.0 → brindle-0.3.1}/PKG-INFO +74 -3
  2. {brindle-0.3.0 → brindle-0.3.1}/README.md +73 -2
  3. {brindle-0.3.0 → brindle-0.3.1}/pyproject.toml +1 -1
  4. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/agents.py +47 -5
  5. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cli.py +20 -3
  6. brindle-0.3.1/src/brindle/company_identity.py +531 -0
  7. brindle-0.3.1/src/brindle/company_login.py +416 -0
  8. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cull.py +12 -4
  9. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/doctor.py +59 -2
  10. brindle-0.3.1/src/brindle/identity_lock.py +71 -0
  11. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/keys_cmds.py +18 -0
  12. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/legacy.py +12 -1
  13. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/mcp_server.py +16 -0
  14. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pane_auth.py +34 -3
  15. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/account.py +10 -0
  16. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/team_policy.py +243 -2
  17. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/providers.py +116 -0
  18. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/rewind.py +2 -1
  19. brindle-0.3.1/src/brindle/self_serve.py +344 -0
  20. brindle-0.3.1/src/brindle/signin_pause.py +384 -0
  21. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/tasks.py +8 -0
  22. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/view.py +12 -1
  23. {brindle-0.3.0 → brindle-0.3.1}/.gitignore +0 -0
  24. {brindle-0.3.0 → brindle-0.3.1}/LICENSE +0 -0
  25. {brindle-0.3.0 → brindle-0.3.1}/SCHEDULE-A +0 -0
  26. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/__init__.py +0 -0
  27. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/__main__.py +0 -0
  28. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/account.py +0 -0
  29. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/airgap.py +0 -0
  30. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/antigravity.py +0 -0
  31. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/autopilot.py +0 -0
  32. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/budget.py +0 -0
  33. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/__init__.py +0 -0
  34. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  35. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-codex.md +0 -0
  36. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  37. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-light.md +0 -0
  38. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-local.md +0 -0
  39. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer.md +0 -0
  40. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/qa.md +0 -0
  41. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  42. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  43. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/reviewer.md +0 -0
  44. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/subagent.md +0 -0
  45. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/supervisor.md +0 -0
  46. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/__init__.py +0 -0
  47. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/security/backend.md +0 -0
  48. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  49. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/tests/only.md +0 -0
  50. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_adapters.py +0 -0
  51. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_client.py +0 -0
  52. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_federation.py +0 -0
  53. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_hosts.py +0 -0
  54. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/codemap.py +0 -0
  55. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/codex_hook.py +0 -0
  56. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/config.py +0 -0
  57. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/conflicts.py +0 -0
  58. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cost.py +0 -0
  59. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cost_estimate.py +0 -0
  60. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/db.py +0 -0
  61. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/demo.py +0 -0
  62. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/detect.py +0 -0
  63. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/escapes.py +0 -0
  64. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/events.py +0 -0
  65. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/gates.py +0 -0
  66. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/git.py +0 -0
  67. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/guardrails.py +0 -0
  68. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/history.py +0 -0
  69. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/inbox.py +0 -0
  70. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/keystore.py +0 -0
  71. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/learned_rules.py +0 -0
  72. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/learning.py +0 -0
  73. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/model_access.py +0 -0
  74. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/__init__.py +0 -0
  75. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/client.py +0 -0
  76. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/loop.py +0 -0
  77. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/permissions.py +0 -0
  78. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/runner.py +0 -0
  79. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/serve.py +0 -0
  80. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/tools.py +0 -0
  81. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/permissions.py +0 -0
  82. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pipeline.py +0 -0
  83. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/plugins.py +0 -0
  84. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/policy.py +0 -0
  85. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pool.py +0 -0
  86. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pricing.py +0 -0
  87. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/__init__.py +0 -0
  88. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/_files.py +0 -0
  89. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/audit_chain.py +0 -0
  90. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/audit_export.py +0 -0
  91. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/auth.py +0 -0
  92. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/brief_learning.py +0 -0
  93. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/cost_centers.py +0 -0
  94. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/credentials.py +0 -0
  95. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/features.py +0 -0
  96. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/keys.py +0 -0
  97. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/learning.py +0 -0
  98. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/license.py +0 -0
  99. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/loopback.py +0 -0
  100. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/managed_models.py +0 -0
  101. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/org_profiles.py +0 -0
  102. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/orgkey.py +0 -0
  103. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/rollout.py +0 -0
  104. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/settings_sync.py +0 -0
  105. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/status.py +0 -0
  106. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/team_events.py +0 -0
  107. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/procs.py +0 -0
  108. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/profiles.py +0 -0
  109. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/quota.py +0 -0
  110. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/repo_cmds.py +0 -0
  111. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/repos.py +0 -0
  112. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/rule_checks.py +0 -0
  113. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/savings.py +0 -0
  114. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/scratch.py +0 -0
  115. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/secrets.py +0 -0
  116. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/services.py +0 -0
  117. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/sessions.py +0 -0
  118. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/sidebar_follow.py +0 -0
  119. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/status_cache.py +0 -0
  120. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/tmux.py +0 -0
  121. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/usage.py +0 -0
  122. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/watch.py +0 -0
  123. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/watch_costs.py +0 -0
  124. {brindle-0.3.0 → brindle-0.3.1}/src/brindle/workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
5
  Project-URL: Homepage, https://pawdelta.com/brindle/
6
6
  Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
@@ -89,7 +89,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
89
89
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
90
90
 
91
91
  `brindle --version` prints the installed version; the tmux status bar of every brindle
92
- session shows it too (`brindle 0.3.0`). A session started before an upgrade keeps
92
+ session shows it too (`brindle 0.3.1`). A session started before an upgrade keeps
93
93
  running the old code, and shows the old number, until you restart it.
94
94
 
95
95
  ## Quick start
@@ -363,6 +363,7 @@ your own status line prints, so what you see doesn't change.
363
363
  | `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
364
364
  | `brindle learning seed` | Pro: sends this repo's finished routing history to the hosted learner once, so it starts with what this machine has seen. Only the usual coarse record fields leave (kind/size one-hots, counters, HMAC keys), never task text or paths. A second run sends nothing new; decisions already sent live are skipped |
365
365
  | `brindle account [login\|logout\|status\|upgrade\|portal\|seats\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
366
+ | `brindle login` | the same as `brindle account login`; when your org's policy has an `agent_setup` block it also signs you in to the Claude and Codex route the company chose (AWS SSO profile, `gcloud`, `az`, `claude auth login`, `codex login`), stores the Claude Code environment for brindle's panes in `~/.brindle/config.json`, and checks the pinned models. `brindle doctor --fix` does the same |
366
367
  | `brindle audit verify\|export\|pubkey\|ship\|prune` | the local tamper-evident audit log, and shipping it to your SIEM (brindle Enterprise; see "Audit log" below) |
367
368
  | `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
368
369
  | `brindle sidebar` | bring this session's sidebar into the tmux session you're in (also `Ctrl-b S` in a window without one); restarts it if it was closed |
@@ -762,6 +763,7 @@ workflow.
762
763
 
763
764
  ```sh
764
765
  brindle account # paid features: what you have, how to use them, how to get the rest
766
+ brindle login # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
765
767
  brindle account login # opens your browser to sign in (--device: enter a code instead, e.g. over SSH); brindle checks the plan offline from then on
766
768
  brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
767
769
  brindle account savings # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
@@ -778,6 +780,51 @@ brindle account org member policy-role <member> <role|none> # give a member a
778
780
  brindle account org company [link <org_id> | unlink] # link orgs you own into one company; learning is pooled only within it
779
781
  ```
780
782
 
783
+ **Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
784
+ Claude and Codex are still signed in (`claude auth status` or the cloud route's
785
+ identity, re-checked only when its credentials change; `codex login status`;
786
+ Antigravity can't be checked). If one is signed out, its running agents pause
787
+ with their worktrees, commits and conversations kept, and the supervisor and
788
+ your session are told; a timeout or missing CLI never pauses, it warns once.
789
+ A wrong company identity pauses them only when the org policy enforces it
790
+ (Enterprise); otherwise it warns once. While it enforces, Claude agents run
791
+ on exactly the route brindle checked: a profile's env lines can't change the
792
+ backend, account, key or proxy, and other cloud and Claude credential variables
793
+ (`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
794
+ environment or `brindle keys` are left out of their panes, so those agents
795
+ can't use your own AWS, Google Cloud or Azure access in their shell either.
796
+ Claude Code's own settings files can still set variables inside it; to rule
797
+ that out too, use Claude Code's managed settings. After `brindle login`,
798
+ `brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
799
+ into their conversations, but only as the account they started under: if
800
+ someone else is signed in (a personal login, say) they stay paused until the
801
+ original account is back. Your own chat session is never paused, only the
802
+ workers. A cloud sign-in is re-checked only when its credential files or
803
+ variables change, so a token that expires silently isn't noticed until then.
804
+ `brindle doctor` lists agents paused for sign-in.
805
+
806
+ **Sessions stay with their identity.** Each Claude agent records the identity it
807
+ launched under (Claude org, AWS account and role, GCP project, Azure
808
+ subscription; ids only, never tokens). While a different identity is signed in,
809
+ that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
810
+ it, message or hand off to it, start a new agent in its worktree (including
811
+ naming its branch for a new worker), or `peek` at it, with "This session
812
+ belongs to <org/account>; sign in as it to continue (`brindle login`)".
813
+ `brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
814
+ `locked: <org/account>` without its conversation (usage, last activity, turn
815
+ summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
816
+ session under another identity. Agents with no recorded identity (started before
817
+ this, or without a company setup) are unaffected, and so is everything while the
818
+ current identity can't be told (signed out, CLI missing).
819
+
820
+ This is a guard inside brindle, not a sandbox: on one OS user account the
821
+ worktrees, transcripts and credential files stay readable outside brindle (a
822
+ shell, another tool). Real separation needs a separate OS user or your
823
+ company's device management. Codex and Antigravity can't be told apart by
824
+ account (brindle only sees "signed in"), so their sessions are paused on
825
+ sign-out but can't be locked to one account; isolation applies to Claude routes
826
+ (subscription, Bedrock, Vertex, Foundry).
827
+
781
828
  ### Several repos in one session (brindle Pro)
782
829
 
783
830
  A session can work across several repos. Attach another local repo you own
@@ -1258,7 +1305,7 @@ it's signed in without a plan that includes it, is reported to its supervisor.
1258
1305
  | Provider | Key login (environment variables) |
1259
1306
  |---|---|
1260
1307
  | `claude` | yes: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or Bedrock/Vertex/Foundry (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`) |
1261
- | `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY` |
1308
+ | `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY`. With Codex signed in with ChatGPT (checked on Codex CLI 0.162.0): interactive `codex`, which brindle runs in agent panes, ignores both keys and uses the ChatGPT plan, and only `codex login --with-api-key` switches it to a key; headless `codex exec` (Brindle-CI) bills `CODEX_API_KEY` over the login and ignores `OPENAI_API_KEY`. `brindle doctor` and `brindle keys set` say so, and `auth: api_key` on an interactive Codex profile is refused under a ChatGPT login (run `codex login --with-api-key`, or use `auth: subscription`) |
1262
1309
  | `antigravity` | `GEMINI_API_KEY`, only with `"modelProvider": "gemini"` in `agy`'s `settings.json` (see [Google Antigravity](#google-antigravity)); otherwise `agy`'s own browser sign-in |
1263
1310
 
1264
1311
  Workers that run unattended should use key login where the CLI supports it.
@@ -1666,6 +1713,30 @@ marks the agent idle and delivers any queued message. Codex has no Stop hook, so
1666
1713
  an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
1667
1714
  going, with the same limit on reminders as Claude Code.
1668
1715
 
1716
+ ### Using your company's access
1717
+
1718
+ Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
1719
+ Foundry, a Team or Enterprise plan) and Codex, without handling keys:
1720
+
1721
+ - **Your org sets it up (brindle Team).** When your org's policy has an
1722
+ `agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
1723
+ in to the route the company chose and `brindle doctor` shows which identity
1724
+ each agent uses. It always wins over anything below.
1725
+ - **You set it up yourself (any plan).** When your org has no `agent_setup`,
1726
+ `brindle login` looks, read-only, at what this machine is already signed in
1727
+ to: Claude Code's managed settings (if your IT configures Claude Code,
1728
+ brindle just uses it), `claude auth status`, AWS SSO profiles in
1729
+ `~/.aws/config`, the active `gcloud` project, the active `az` subscription
1730
+ and `codex login status`. It never reads tokens or credential files. It
1731
+ proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
1732
+ the default is no, and with no terminal it is always no. A yes is saved as
1733
+ `"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
1734
+ policy's (`enforce` is never applied to it), and `brindle doctor` labels it
1735
+ as self-serve and says who is billed.
1736
+
1737
+ You must be authorized to use any company credentials you connect: brindle only
1738
+ signs in with the access you already have, it doesn't grant any.
1739
+
1669
1740
  ## Google Antigravity
1670
1741
 
1671
1742
  brindle runs Google Antigravity's terminal agent, `agy`, as well as Claude Code and
@@ -59,7 +59,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
59
59
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
60
60
 
61
61
  `brindle --version` prints the installed version; the tmux status bar of every brindle
62
- session shows it too (`brindle 0.3.0`). A session started before an upgrade keeps
62
+ session shows it too (`brindle 0.3.1`). A session started before an upgrade keeps
63
63
  running the old code, and shows the old number, until you restart it.
64
64
 
65
65
  ## Quick start
@@ -333,6 +333,7 @@ your own status line prints, so what you see doesn't change.
333
333
  | `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
334
334
  | `brindle learning seed` | Pro: sends this repo's finished routing history to the hosted learner once, so it starts with what this machine has seen. Only the usual coarse record fields leave (kind/size one-hots, counters, HMAC keys), never task text or paths. A second run sends nothing new; decisions already sent live are skipped |
335
335
  | `brindle account [login\|logout\|status\|upgrade\|portal\|seats\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
336
+ | `brindle login` | the same as `brindle account login`; when your org's policy has an `agent_setup` block it also signs you in to the Claude and Codex route the company chose (AWS SSO profile, `gcloud`, `az`, `claude auth login`, `codex login`), stores the Claude Code environment for brindle's panes in `~/.brindle/config.json`, and checks the pinned models. `brindle doctor --fix` does the same |
336
337
  | `brindle audit verify\|export\|pubkey\|ship\|prune` | the local tamper-evident audit log, and shipping it to your SIEM (brindle Enterprise; see "Audit log" below) |
337
338
  | `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
338
339
  | `brindle sidebar` | bring this session's sidebar into the tmux session you're in (also `Ctrl-b S` in a window without one); restarts it if it was closed |
@@ -732,6 +733,7 @@ workflow.
732
733
 
733
734
  ```sh
734
735
  brindle account # paid features: what you have, how to use them, how to get the rest
736
+ brindle login # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
735
737
  brindle account login # opens your browser to sign in (--device: enter a code instead, e.g. over SSH); brindle checks the plan offline from then on
736
738
  brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
737
739
  brindle account savings # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
@@ -748,6 +750,51 @@ brindle account org member policy-role <member> <role|none> # give a member a
748
750
  brindle account org company [link <org_id> | unlink] # link orgs you own into one company; learning is pooled only within it
749
751
  ```
750
752
 
753
+ **Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
754
+ Claude and Codex are still signed in (`claude auth status` or the cloud route's
755
+ identity, re-checked only when its credentials change; `codex login status`;
756
+ Antigravity can't be checked). If one is signed out, its running agents pause
757
+ with their worktrees, commits and conversations kept, and the supervisor and
758
+ your session are told; a timeout or missing CLI never pauses, it warns once.
759
+ A wrong company identity pauses them only when the org policy enforces it
760
+ (Enterprise); otherwise it warns once. While it enforces, Claude agents run
761
+ on exactly the route brindle checked: a profile's env lines can't change the
762
+ backend, account, key or proxy, and other cloud and Claude credential variables
763
+ (`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
764
+ environment or `brindle keys` are left out of their panes, so those agents
765
+ can't use your own AWS, Google Cloud or Azure access in their shell either.
766
+ Claude Code's own settings files can still set variables inside it; to rule
767
+ that out too, use Claude Code's managed settings. After `brindle login`,
768
+ `brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
769
+ into their conversations, but only as the account they started under: if
770
+ someone else is signed in (a personal login, say) they stay paused until the
771
+ original account is back. Your own chat session is never paused, only the
772
+ workers. A cloud sign-in is re-checked only when its credential files or
773
+ variables change, so a token that expires silently isn't noticed until then.
774
+ `brindle doctor` lists agents paused for sign-in.
775
+
776
+ **Sessions stay with their identity.** Each Claude agent records the identity it
777
+ launched under (Claude org, AWS account and role, GCP project, Azure
778
+ subscription; ids only, never tokens). While a different identity is signed in,
779
+ that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
780
+ it, message or hand off to it, start a new agent in its worktree (including
781
+ naming its branch for a new worker), or `peek` at it, with "This session
782
+ belongs to <org/account>; sign in as it to continue (`brindle login`)".
783
+ `brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
784
+ `locked: <org/account>` without its conversation (usage, last activity, turn
785
+ summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
786
+ session under another identity. Agents with no recorded identity (started before
787
+ this, or without a company setup) are unaffected, and so is everything while the
788
+ current identity can't be told (signed out, CLI missing).
789
+
790
+ This is a guard inside brindle, not a sandbox: on one OS user account the
791
+ worktrees, transcripts and credential files stay readable outside brindle (a
792
+ shell, another tool). Real separation needs a separate OS user or your
793
+ company's device management. Codex and Antigravity can't be told apart by
794
+ account (brindle only sees "signed in"), so their sessions are paused on
795
+ sign-out but can't be locked to one account; isolation applies to Claude routes
796
+ (subscription, Bedrock, Vertex, Foundry).
797
+
751
798
  ### Several repos in one session (brindle Pro)
752
799
 
753
800
  A session can work across several repos. Attach another local repo you own
@@ -1228,7 +1275,7 @@ it's signed in without a plan that includes it, is reported to its supervisor.
1228
1275
  | Provider | Key login (environment variables) |
1229
1276
  |---|---|
1230
1277
  | `claude` | yes: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or Bedrock/Vertex/Foundry (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`) |
1231
- | `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY` |
1278
+ | `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY`. With Codex signed in with ChatGPT (checked on Codex CLI 0.162.0): interactive `codex`, which brindle runs in agent panes, ignores both keys and uses the ChatGPT plan, and only `codex login --with-api-key` switches it to a key; headless `codex exec` (Brindle-CI) bills `CODEX_API_KEY` over the login and ignores `OPENAI_API_KEY`. `brindle doctor` and `brindle keys set` say so, and `auth: api_key` on an interactive Codex profile is refused under a ChatGPT login (run `codex login --with-api-key`, or use `auth: subscription`) |
1232
1279
  | `antigravity` | `GEMINI_API_KEY`, only with `"modelProvider": "gemini"` in `agy`'s `settings.json` (see [Google Antigravity](#google-antigravity)); otherwise `agy`'s own browser sign-in |
1233
1280
 
1234
1281
  Workers that run unattended should use key login where the CLI supports it.
@@ -1636,6 +1683,30 @@ marks the agent idle and delivers any queued message. Codex has no Stop hook, so
1636
1683
  an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
1637
1684
  going, with the same limit on reminders as Claude Code.
1638
1685
 
1686
+ ### Using your company's access
1687
+
1688
+ Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
1689
+ Foundry, a Team or Enterprise plan) and Codex, without handling keys:
1690
+
1691
+ - **Your org sets it up (brindle Team).** When your org's policy has an
1692
+ `agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
1693
+ in to the route the company chose and `brindle doctor` shows which identity
1694
+ each agent uses. It always wins over anything below.
1695
+ - **You set it up yourself (any plan).** When your org has no `agent_setup`,
1696
+ `brindle login` looks, read-only, at what this machine is already signed in
1697
+ to: Claude Code's managed settings (if your IT configures Claude Code,
1698
+ brindle just uses it), `claude auth status`, AWS SSO profiles in
1699
+ `~/.aws/config`, the active `gcloud` project, the active `az` subscription
1700
+ and `codex login status`. It never reads tokens or credential files. It
1701
+ proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
1702
+ the default is no, and with no terminal it is always no. A yes is saved as
1703
+ `"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
1704
+ policy's (`enforce` is never applied to it), and `brindle doctor` labels it
1705
+ as self-serve and says who is billed.
1706
+
1707
+ You must be authorized to use any company credentials you connect: brindle only
1708
+ signs in with the access you already have, it doesn't grant any.
1709
+
1639
1710
  ## Google Antigravity
1640
1711
 
1641
1712
  brindle runs Google Antigravity's terminal agent, `agy`, as well as Claude Code and
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -20,7 +20,7 @@ import sys
20
20
  import time
21
21
  import uuid
22
22
 
23
- from brindle import git, keystore, pane_auth, providers, secrets, tmux, workspaces
23
+ from brindle import company_identity, company_login, git, keystore, pane_auth, providers, secrets, tmux, workspaces
24
24
  from brindle.config import RepoConfig
25
25
  from brindle.db import DB, Agent, Workspace
26
26
  from brindle.profiles import load_profile, missing_add_dirs
@@ -183,7 +183,19 @@ def agent_env(ws: Workspace, agent_id: str, agent: Agent | None = None,
183
183
  # Claude Code's own managed settings (apiKeyHelper, a managed env) pick
184
184
  # the credential and backend: brindle adds no provider_config env there.
185
185
  org = agent.provider == "claude" and providers.claude_org_managed()
186
- env = {**profile_env, **({} if org else managed_models.agent_env(m, agent.provider)), **env}
186
+ # The company's Claude Code env (`brindle login`): the lowest layer, so a
187
+ # profile's env lines and the org's managed provider both beat it; none
188
+ # at all under Claude Code's managed settings (stored_env).
189
+ company = {} if agent.provider != "claude" else company_login.stored_env()
190
+ managed_env = {} if org else managed_models.agent_env(m, agent.provider)
191
+ enforced = None if agent.provider != "claude" or org else company_identity.enforced_route_env()
192
+ if enforced is None:
193
+ env = {**company, **profile_env, **managed_env, **env}
194
+ else:
195
+ # An enforced company identity (Enterprise): the pane runs on the route
196
+ # launch_problem checked, whatever a profile's env lines say.
197
+ profile_env = {k: v for k, v in profile_env.items() if company_identity.profile_may_set(k)}
198
+ env = {**profile_env, **managed_env, **enforced, **env}
187
199
  if agent is not None and preload_tools(agent, ws):
188
200
  # Claude Code defers MCP tools and loads them on demand, which costs a
189
201
  # worker an extra round trip at the moment it's told to report (and
@@ -262,7 +274,9 @@ def spawn(
262
274
  finish line run it as a Claude Code ``/goal``. With ``autopilot``, the
263
275
  agent is a session root that drives toward a goal (see brindle.autopilot)."""
264
276
  from brindle import autopilot as pilot
277
+ from brindle import identity_lock
265
278
 
279
+ identity_lock.ensure_workspace(db, ws) # no new agent in another identity's worktree
266
280
  profile = load_profile(profile_name, ws.repo_root)
267
281
  provider = get_provider(provider_name or profile.provider)
268
282
  agent_id = new_id()
@@ -440,6 +454,12 @@ def _open_window(db: DB, agent: Agent, ws: Workspace, name: str, argv: list[str]
440
454
  org_env = managed_models.org_key_env(m, agent.provider)
441
455
  except managed_models.ManagedUnavailable as e:
442
456
  raise AgentError(f"managed models: {e}") from None
457
+ # An enforced company identity: also every account picker the tmux server
458
+ # would hand the pane or `brindle keys` holds for it (pane_auth covers this process's).
459
+ route = None if agent.provider != "claude" or org_managed else company_identity.enforced_route_env()
460
+ if route is not None:
461
+ extra = company_identity.enforced_deny(route, tmux.inherited_names(ws.tmux_session), credentials)
462
+ deny = (*deny, *(n for n in extra if n not in deny))
443
463
  # Claude Code's managed settings supply the credential: no stored Claude key.
444
464
  no_stored = set(deny) | (set(providers._ENV_AUTH["claude"]) if org_managed else set())
445
465
  env = {**keystore.pane_keys(credentials - no_stored, {**env, **org_env}), **env, **org_env}
@@ -612,6 +632,14 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
612
632
  db.set_status(agent.id, status)
613
633
  agent.status = status
614
634
  return
635
+ # An org that enforces its company sign-in (Enterprise) refuses a
636
+ # different identity; one that can't be told never blocks.
637
+ why = company_identity.launch_problem(provider.name)
638
+ if why:
639
+ raise AgentError(why)
640
+ from brindle import signin_pause
641
+
642
+ signin_pause.record_launch(agent) # who it runs as, so a resume never crosses accounts
615
643
  # What the agent will run with: the managed provider's variables, and
616
644
  # no personal keys (blank, so the launcher's own copy doesn't count).
617
645
  from brindle.pro import managed_models
@@ -906,10 +934,14 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
906
934
  restart in their own workspaces. Claude Code picks up its previous
907
935
  conversation (--resume); other CLIs restart on their original task.
908
936
  With ``only``, just those agents (if paused) restart."""
937
+ from brindle import identity_lock
938
+
939
+ candidates = [a for a in tree(db, root_id)
940
+ if a.status == "paused" and (only is None or a.id in only)]
941
+ for a in candidates: # all or nothing: a session never half-resumes under another identity
942
+ identity_lock.ensure(a)
909
943
  resumed = []
910
- for a in tree(db, root_id):
911
- if a.status != "paused" or (only is not None and a.id not in only):
912
- continue
944
+ for a in candidates:
913
945
  ws = db.get_workspace(a.workspace_id)
914
946
  if ws is None or not os.path.isdir(ws.path):
915
947
  continue
@@ -1138,7 +1170,10 @@ def send_message(db: DB, to_id: str, body: str, sender_id: str | None = None,
1138
1170
  """Deliver now if the agent is idle; otherwise queue until it is.
1139
1171
  Returns ``"delivered"`` or ``"queued"``. ``person`` marks a message from
1140
1172
  the person running brindle, which is always pushed."""
1173
+ from brindle import identity_lock
1174
+
1141
1175
  agent = get(db, to_id)
1176
+ identity_lock.ensure(agent)
1142
1177
  if not person and runs_process(agent) and is_alive(agent) and pulls_messages(db, agent):
1143
1178
  # Keep the message unread and deliver only a notice, once for everything unread.
1144
1179
  db.enqueue_held(agent.id, format_message(db, body, sender_id), sender_id)
@@ -1648,6 +1683,13 @@ def delegate(
1648
1683
  raise AgentError(str(e)) from e
1649
1684
  if plan_first is None:
1650
1685
  plan_first = load_repo_config(caller_ws.repo_root).plan_first
1686
+ from brindle import identity_lock
1687
+
1688
+ identity_lock.ensure_workspace(db, caller_ws)
1689
+ if isolate and branch: # naming a branch must not reuse another identity's worktree
1690
+ for other in db.find_workspaces(caller_ws.repo_root):
1691
+ if other.branch == branch:
1692
+ identity_lock.ensure_workspace(db, other)
1651
1693
  if isolate:
1652
1694
  caller_ws = workspaces.refresh_branch(db, caller_ws)
1653
1695
  # merge_into applies to the supervisor's workers; a worker's own
@@ -13,7 +13,7 @@ from typing import Optional
13
13
 
14
14
  import typer
15
15
 
16
- from brindle import agents, git, tmux, view, workspaces
16
+ from brindle import agents, git, identity_lock, tmux, view, workspaces
17
17
  from brindle import history as history_mod
18
18
  from brindle import keys_cmds, repo_cmds
19
19
  from brindle.usage import format_tokens
@@ -792,7 +792,7 @@ def autopilot_cmd(
792
792
 
793
793
  @app.command()
794
794
  def doctor(
795
- fix: bool = typer.Option(False, "--fix", help="Offer to let Claude Code use an exported ANTHROPIC_API_KEY without asking."),
795
+ fix: bool = typer.Option(False, "--fix", help="Offer to let Claude Code use an exported ANTHROPIC_API_KEY without asking, and agy a GEMINI_API_KEY (modelProvider \"gemini\")."),
796
796
  ) -> None:
797
797
  """Check that brindle has what it needs, and say what to do about anything missing.
798
798
 
@@ -1655,6 +1655,17 @@ def learning_seed() -> None:
1655
1655
  "help_option_names": []}) # --help is the plugin's: it lists every subcommand
1656
1656
  def account(ctx: typer.Context) -> None:
1657
1657
  """brindle Pro/Team: paid features, login, upgrade, billing, orgs. Bare `brindle account` shows what you have."""
1658
+ _run_account(list(ctx.args))
1659
+
1660
+
1661
+ @app.command("login", context_settings={"allow_extra_args": True, "ignore_unknown_options": True,
1662
+ "help_option_names": []})
1663
+ def login(ctx: typer.Context) -> None:
1664
+ """Log in to brindle and set up your company's agents: the same as `brindle account login`."""
1665
+ _run_account(["login", *ctx.args])
1666
+
1667
+
1668
+ def _run_account(args: list[str]) -> None:
1658
1669
  from brindle import account as account_mod
1659
1670
  from brindle.config import load_repo_config
1660
1671
 
@@ -1668,7 +1679,7 @@ def account(ctx: typer.Context) -> None:
1668
1679
  from brindle.config import RepoConfig
1669
1680
 
1670
1681
  cfg = RepoConfig()
1671
- raise typer.Exit(account_mod.run(cfg, repo_root, list(ctx.args), echo=typer.echo))
1682
+ raise typer.Exit(account_mod.run(cfg, repo_root, args, echo=typer.echo))
1672
1683
 
1673
1684
 
1674
1685
  # -- brindle audit (brindle Enterprise: the local tamper-evident audit chain) ---------------------
@@ -2141,6 +2152,7 @@ def agent_peek(agent_id: str, lines: int = typer.Option(40, "--lines", "-n")) ->
2141
2152
  """Print the last lines of an agent's terminal."""
2142
2153
  db = DB()
2143
2154
  a = _run(agents.get, db, agent_id)
2155
+ _run(identity_lock.ensure, a)
2144
2156
  if not a.tmux_window:
2145
2157
  typer.secho(f"{a.id} has no terminal (it runs as its supervisor's own subagent)"
2146
2158
  if not agents.runs_process(a) else f"{a.id} has no terminal", fg="red", err=True)
@@ -2158,6 +2170,11 @@ def agent_turns(agent_id: str) -> None:
2158
2170
  ws = db.get_workspace(a.workspace_id)
2159
2171
  if ws is None:
2160
2172
  _fail(f"{a.id}'s workspace is gone")
2173
+ from brindle import identity_lock
2174
+
2175
+ who = identity_lock.locked_by(a.id)
2176
+ if who:
2177
+ _fail(f"{a.id} is locked: {who}. {identity_lock.message(who)}.")
2161
2178
  typer.echo(rewind.format_turns(ws.repo_root, a.id))
2162
2179
 
2163
2180