brindle 0.3.0__tar.gz → 0.3.2__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.2}/PKG-INFO +90 -3
  2. {brindle-0.3.0 → brindle-0.3.2}/README.md +89 -2
  3. {brindle-0.3.0 → brindle-0.3.2}/pyproject.toml +1 -1
  4. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/agents.py +53 -8
  5. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/antigravity.py +111 -20
  6. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/autopilot.py +4 -1
  7. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/cli.py +28 -7
  8. brindle-0.3.2/src/brindle/company_identity.py +531 -0
  9. brindle-0.3.2/src/brindle/company_login.py +416 -0
  10. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/config.py +6 -2
  11. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/cull.py +12 -4
  12. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/doctor.py +59 -2
  13. brindle-0.3.2/src/brindle/identity_lock.py +71 -0
  14. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/keys_cmds.py +18 -0
  15. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/legacy.py +12 -1
  16. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/mcp_server.py +16 -0
  17. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pane_auth.py +34 -3
  18. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/permissions.py +183 -1
  19. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/account.py +16 -1
  20. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/auth.py +1 -1
  21. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/team_policy.py +243 -2
  22. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/providers.py +147 -1
  23. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/repos.py +5 -1
  24. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/rewind.py +2 -1
  25. brindle-0.3.2/src/brindle/self_serve.py +344 -0
  26. brindle-0.3.2/src/brindle/signin_pause.py +384 -0
  27. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/tasks.py +8 -0
  28. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/view.py +12 -1
  29. {brindle-0.3.0 → brindle-0.3.2}/.gitignore +0 -0
  30. {brindle-0.3.0 → brindle-0.3.2}/LICENSE +0 -0
  31. {brindle-0.3.0 → brindle-0.3.2}/SCHEDULE-A +0 -0
  32. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/__init__.py +0 -0
  33. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/__main__.py +0 -0
  34. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/account.py +0 -0
  35. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/airgap.py +0 -0
  36. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/budget.py +0 -0
  37. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/__init__.py +0 -0
  38. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  39. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/developer-codex.md +0 -0
  40. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  41. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/developer-light.md +0 -0
  42. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/developer-local.md +0 -0
  43. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/developer.md +0 -0
  44. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/qa.md +0 -0
  45. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  46. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  47. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/reviewer.md +0 -0
  48. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/subagent.md +0 -0
  49. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_agents/supervisor.md +0 -0
  50. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_rules/__init__.py +0 -0
  51. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_rules/security/backend.md +0 -0
  52. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  53. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/builtin_rules/tests/only.md +0 -0
  54. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/ci_adapters.py +0 -0
  55. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/ci_client.py +0 -0
  56. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/ci_federation.py +0 -0
  57. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/ci_hosts.py +0 -0
  58. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/codemap.py +0 -0
  59. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/codex_hook.py +0 -0
  60. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/conflicts.py +0 -0
  61. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/cost.py +0 -0
  62. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/cost_estimate.py +0 -0
  63. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/db.py +0 -0
  64. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/demo.py +0 -0
  65. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/detect.py +0 -0
  66. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/escapes.py +0 -0
  67. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/events.py +0 -0
  68. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/gates.py +0 -0
  69. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/git.py +0 -0
  70. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/guardrails.py +0 -0
  71. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/history.py +0 -0
  72. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/inbox.py +0 -0
  73. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/keystore.py +0 -0
  74. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/learned_rules.py +0 -0
  75. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/learning.py +0 -0
  76. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/model_access.py +0 -0
  77. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/__init__.py +0 -0
  78. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/client.py +0 -0
  79. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/loop.py +0 -0
  80. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/permissions.py +0 -0
  81. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/runner.py +0 -0
  82. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/serve.py +0 -0
  83. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/native/tools.py +0 -0
  84. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pipeline.py +0 -0
  85. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/plugins.py +0 -0
  86. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/policy.py +0 -0
  87. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pool.py +0 -0
  88. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pricing.py +0 -0
  89. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/__init__.py +0 -0
  90. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/_files.py +0 -0
  91. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/audit_chain.py +0 -0
  92. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/audit_export.py +0 -0
  93. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/brief_learning.py +0 -0
  94. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/cost_centers.py +0 -0
  95. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/credentials.py +0 -0
  96. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/features.py +0 -0
  97. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/keys.py +0 -0
  98. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/learning.py +0 -0
  99. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/license.py +0 -0
  100. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/loopback.py +0 -0
  101. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/managed_models.py +0 -0
  102. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/org_profiles.py +0 -0
  103. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/orgkey.py +0 -0
  104. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/rollout.py +0 -0
  105. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/settings_sync.py +0 -0
  106. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/status.py +0 -0
  107. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/pro/team_events.py +0 -0
  108. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/procs.py +0 -0
  109. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/profiles.py +0 -0
  110. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/quota.py +0 -0
  111. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/repo_cmds.py +0 -0
  112. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/rule_checks.py +0 -0
  113. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/savings.py +0 -0
  114. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/scratch.py +0 -0
  115. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/secrets.py +0 -0
  116. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/services.py +0 -0
  117. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/sessions.py +0 -0
  118. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/sidebar_follow.py +0 -0
  119. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/status_cache.py +0 -0
  120. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/tmux.py +0 -0
  121. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/usage.py +0 -0
  122. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/watch.py +0 -0
  123. {brindle-0.3.0 → brindle-0.3.2}/src/brindle/watch_costs.py +0 -0
  124. {brindle-0.3.0 → brindle-0.3.2}/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.2
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.2`). 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 |
@@ -538,6 +539,7 @@ Autopilot, merge gates and cleanup:
538
539
  | `merge_into` | none | branch that worker branches are cut from and merge into, whatever branch the supervisor is on |
539
540
  | `auto_merge_default_branch` | `false` | let the pipeline merge into the repo's default branch (origin HEAD, else `main`/`master`) on its own; by default it sends a "needs you" message instead, and you run `merge_workspace` yourself (manual merges are never gated) |
540
541
  | `permission_policy` | `"off"` | `"on"`: brindle answers Claude Code workers' permission prompts from its rules (see "Permission policy" below); off, the prompts behave as they always did |
542
+ | `agy_approvals` | `"prompt"` | `"brindle"`: with the policy on, Antigravity workers run without approval prompts and brindle's hook allows or denies every call (see "Antigravity" under "Permission policy") |
541
543
  | `rules` | `[]` | standing rules for the supervisor, e.g. `["Fix review findings without asking", "Validate options before offering them"]`; added to every supervisor's brief. Rules in `~/.brindle/config.json`, the repo's config and `config.local.json` are all kept, so a team's rules and your own add up |
542
544
  | `plan_first` | `false` | workers propose a plan (`submit_plan`) and wait for `approve_plan` before editing |
543
545
  | `overlap` | `"block"` | a task whose `files` overlap a running task's is refused (`"warn"` starts it with a warning) |
@@ -725,6 +727,21 @@ does it by hand. To undo, turn `permission_policy` off and run `sync-agy`: brind
725
727
  removes exactly its own entries. brindle doesn't learn from agy approvals (its
726
728
  hook runs for every tool call, so a tool running doesn't mean you approved it).
727
729
 
730
+ With `"agy_approvals": "brindle"` (and the policy on), brindle's hook is the
731
+ only gate instead: agy workers start with `--dangerously-skip-permissions`,
732
+ under which agy still honors a hook's deny, and the hook runs on every tool,
733
+ subagents' calls included. It allows what a rule allows (a repo's checks
734
+ included), the shell commands in the profile's `allowed_tools` (what a Claude
735
+ Code worker on it runs without asking), files inside the workspace given as one
736
+ absolute path (writes under `.agents/`, `.git/` and
737
+ `.brindle/` only by a rule, since those hold the hook, git's hooks and the
738
+ repo's checks), brindle's own tools and agy's tools that act only on the
739
+ conversation, and denies everything else, telling the agent to
740
+ ask its supervisor; you allow it with `brindle permissions allow`. Any failure
741
+ in the hook is a deny, and a tool brindle doesn't know is denied. Nobody can
742
+ approve a prompt in the agent's pane in this mode. The default, `"prompt"`,
743
+ leaves agy asking as described above.
744
+
728
745
  ## brindle Pro and Team
729
746
 
730
747
  brindle is complete on its own. brindle Pro adds hosted learning (which profile
@@ -762,6 +779,7 @@ workflow.
762
779
 
763
780
  ```sh
764
781
  brindle account # paid features: what you have, how to use them, how to get the rest
782
+ brindle login # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
765
783
  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
784
  brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
767
785
  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 +796,51 @@ brindle account org member policy-role <member> <role|none> # give a member a
778
796
  brindle account org company [link <org_id> | unlink] # link orgs you own into one company; learning is pooled only within it
779
797
  ```
780
798
 
799
+ **Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
800
+ Claude and Codex are still signed in (`claude auth status` or the cloud route's
801
+ identity, re-checked only when its credentials change; `codex login status`;
802
+ Antigravity can't be checked). If one is signed out, its running agents pause
803
+ with their worktrees, commits and conversations kept, and the supervisor and
804
+ your session are told; a timeout or missing CLI never pauses, it warns once.
805
+ A wrong company identity pauses them only when the org policy enforces it
806
+ (Enterprise); otherwise it warns once. While it enforces, Claude agents run
807
+ on exactly the route brindle checked: a profile's env lines can't change the
808
+ backend, account, key or proxy, and other cloud and Claude credential variables
809
+ (`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
810
+ environment or `brindle keys` are left out of their panes, so those agents
811
+ can't use your own AWS, Google Cloud or Azure access in their shell either.
812
+ Claude Code's own settings files can still set variables inside it; to rule
813
+ that out too, use Claude Code's managed settings. After `brindle login`,
814
+ `brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
815
+ into their conversations, but only as the account they started under: if
816
+ someone else is signed in (a personal login, say) they stay paused until the
817
+ original account is back. Your own chat session is never paused, only the
818
+ workers. A cloud sign-in is re-checked only when its credential files or
819
+ variables change, so a token that expires silently isn't noticed until then.
820
+ `brindle doctor` lists agents paused for sign-in.
821
+
822
+ **Sessions stay with their identity.** Each Claude agent records the identity it
823
+ launched under (Claude org, AWS account and role, GCP project, Azure
824
+ subscription; ids only, never tokens). While a different identity is signed in,
825
+ that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
826
+ it, message or hand off to it, start a new agent in its worktree (including
827
+ naming its branch for a new worker), or `peek` at it, with "This session
828
+ belongs to <org/account>; sign in as it to continue (`brindle login`)".
829
+ `brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
830
+ `locked: <org/account>` without its conversation (usage, last activity, turn
831
+ summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
832
+ session under another identity. Agents with no recorded identity (started before
833
+ this, or without a company setup) are unaffected, and so is everything while the
834
+ current identity can't be told (signed out, CLI missing).
835
+
836
+ This is a guard inside brindle, not a sandbox: on one OS user account the
837
+ worktrees, transcripts and credential files stay readable outside brindle (a
838
+ shell, another tool). Real separation needs a separate OS user or your
839
+ company's device management. Codex and Antigravity can't be told apart by
840
+ account (brindle only sees "signed in"), so their sessions are paused on
841
+ sign-out but can't be locked to one account; isolation applies to Claude routes
842
+ (subscription, Bedrock, Vertex, Foundry).
843
+
781
844
  ### Several repos in one session (brindle Pro)
782
845
 
783
846
  A session can work across several repos. Attach another local repo you own
@@ -1258,7 +1321,7 @@ it's signed in without a plan that includes it, is reported to its supervisor.
1258
1321
  | Provider | Key login (environment variables) |
1259
1322
  |---|---|
1260
1323
  | `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` |
1324
+ | `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
1325
  | `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
1326
 
1264
1327
  Workers that run unattended should use key login where the CLI supports it.
@@ -1666,6 +1729,30 @@ marks the agent idle and delivers any queued message. Codex has no Stop hook, so
1666
1729
  an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
1667
1730
  going, with the same limit on reminders as Claude Code.
1668
1731
 
1732
+ ### Using your company's access
1733
+
1734
+ Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
1735
+ Foundry, a Team or Enterprise plan) and Codex, without handling keys:
1736
+
1737
+ - **Your org sets it up (brindle Team).** When your org's policy has an
1738
+ `agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
1739
+ in to the route the company chose and `brindle doctor` shows which identity
1740
+ each agent uses. It always wins over anything below.
1741
+ - **You set it up yourself (any plan).** When your org has no `agent_setup`,
1742
+ `brindle login` looks, read-only, at what this machine is already signed in
1743
+ to: Claude Code's managed settings (if your IT configures Claude Code,
1744
+ brindle just uses it), `claude auth status`, AWS SSO profiles in
1745
+ `~/.aws/config`, the active `gcloud` project, the active `az` subscription
1746
+ and `codex login status`. It never reads tokens or credential files. It
1747
+ proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
1748
+ the default is no, and with no terminal it is always no. A yes is saved as
1749
+ `"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
1750
+ policy's (`enforce` is never applied to it), and `brindle doctor` labels it
1751
+ as self-serve and says who is billed.
1752
+
1753
+ You must be authorized to use any company credentials you connect: brindle only
1754
+ signs in with the access you already have, it doesn't grant any.
1755
+
1669
1756
  ## Google Antigravity
1670
1757
 
1671
1758
  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.2`). 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 |
@@ -508,6 +509,7 @@ Autopilot, merge gates and cleanup:
508
509
  | `merge_into` | none | branch that worker branches are cut from and merge into, whatever branch the supervisor is on |
509
510
  | `auto_merge_default_branch` | `false` | let the pipeline merge into the repo's default branch (origin HEAD, else `main`/`master`) on its own; by default it sends a "needs you" message instead, and you run `merge_workspace` yourself (manual merges are never gated) |
510
511
  | `permission_policy` | `"off"` | `"on"`: brindle answers Claude Code workers' permission prompts from its rules (see "Permission policy" below); off, the prompts behave as they always did |
512
+ | `agy_approvals` | `"prompt"` | `"brindle"`: with the policy on, Antigravity workers run without approval prompts and brindle's hook allows or denies every call (see "Antigravity" under "Permission policy") |
511
513
  | `rules` | `[]` | standing rules for the supervisor, e.g. `["Fix review findings without asking", "Validate options before offering them"]`; added to every supervisor's brief. Rules in `~/.brindle/config.json`, the repo's config and `config.local.json` are all kept, so a team's rules and your own add up |
512
514
  | `plan_first` | `false` | workers propose a plan (`submit_plan`) and wait for `approve_plan` before editing |
513
515
  | `overlap` | `"block"` | a task whose `files` overlap a running task's is refused (`"warn"` starts it with a warning) |
@@ -695,6 +697,21 @@ does it by hand. To undo, turn `permission_policy` off and run `sync-agy`: brind
695
697
  removes exactly its own entries. brindle doesn't learn from agy approvals (its
696
698
  hook runs for every tool call, so a tool running doesn't mean you approved it).
697
699
 
700
+ With `"agy_approvals": "brindle"` (and the policy on), brindle's hook is the
701
+ only gate instead: agy workers start with `--dangerously-skip-permissions`,
702
+ under which agy still honors a hook's deny, and the hook runs on every tool,
703
+ subagents' calls included. It allows what a rule allows (a repo's checks
704
+ included), the shell commands in the profile's `allowed_tools` (what a Claude
705
+ Code worker on it runs without asking), files inside the workspace given as one
706
+ absolute path (writes under `.agents/`, `.git/` and
707
+ `.brindle/` only by a rule, since those hold the hook, git's hooks and the
708
+ repo's checks), brindle's own tools and agy's tools that act only on the
709
+ conversation, and denies everything else, telling the agent to
710
+ ask its supervisor; you allow it with `brindle permissions allow`. Any failure
711
+ in the hook is a deny, and a tool brindle doesn't know is denied. Nobody can
712
+ approve a prompt in the agent's pane in this mode. The default, `"prompt"`,
713
+ leaves agy asking as described above.
714
+
698
715
  ## brindle Pro and Team
699
716
 
700
717
  brindle is complete on its own. brindle Pro adds hosted learning (which profile
@@ -732,6 +749,7 @@ workflow.
732
749
 
733
750
  ```sh
734
751
  brindle account # paid features: what you have, how to use them, how to get the rest
752
+ brindle login # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
735
753
  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
754
  brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
737
755
  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 +766,51 @@ brindle account org member policy-role <member> <role|none> # give a member a
748
766
  brindle account org company [link <org_id> | unlink] # link orgs you own into one company; learning is pooled only within it
749
767
  ```
750
768
 
769
+ **Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
770
+ Claude and Codex are still signed in (`claude auth status` or the cloud route's
771
+ identity, re-checked only when its credentials change; `codex login status`;
772
+ Antigravity can't be checked). If one is signed out, its running agents pause
773
+ with their worktrees, commits and conversations kept, and the supervisor and
774
+ your session are told; a timeout or missing CLI never pauses, it warns once.
775
+ A wrong company identity pauses them only when the org policy enforces it
776
+ (Enterprise); otherwise it warns once. While it enforces, Claude agents run
777
+ on exactly the route brindle checked: a profile's env lines can't change the
778
+ backend, account, key or proxy, and other cloud and Claude credential variables
779
+ (`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
780
+ environment or `brindle keys` are left out of their panes, so those agents
781
+ can't use your own AWS, Google Cloud or Azure access in their shell either.
782
+ Claude Code's own settings files can still set variables inside it; to rule
783
+ that out too, use Claude Code's managed settings. After `brindle login`,
784
+ `brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
785
+ into their conversations, but only as the account they started under: if
786
+ someone else is signed in (a personal login, say) they stay paused until the
787
+ original account is back. Your own chat session is never paused, only the
788
+ workers. A cloud sign-in is re-checked only when its credential files or
789
+ variables change, so a token that expires silently isn't noticed until then.
790
+ `brindle doctor` lists agents paused for sign-in.
791
+
792
+ **Sessions stay with their identity.** Each Claude agent records the identity it
793
+ launched under (Claude org, AWS account and role, GCP project, Azure
794
+ subscription; ids only, never tokens). While a different identity is signed in,
795
+ that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
796
+ it, message or hand off to it, start a new agent in its worktree (including
797
+ naming its branch for a new worker), or `peek` at it, with "This session
798
+ belongs to <org/account>; sign in as it to continue (`brindle login`)".
799
+ `brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
800
+ `locked: <org/account>` without its conversation (usage, last activity, turn
801
+ summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
802
+ session under another identity. Agents with no recorded identity (started before
803
+ this, or without a company setup) are unaffected, and so is everything while the
804
+ current identity can't be told (signed out, CLI missing).
805
+
806
+ This is a guard inside brindle, not a sandbox: on one OS user account the
807
+ worktrees, transcripts and credential files stay readable outside brindle (a
808
+ shell, another tool). Real separation needs a separate OS user or your
809
+ company's device management. Codex and Antigravity can't be told apart by
810
+ account (brindle only sees "signed in"), so their sessions are paused on
811
+ sign-out but can't be locked to one account; isolation applies to Claude routes
812
+ (subscription, Bedrock, Vertex, Foundry).
813
+
751
814
  ### Several repos in one session (brindle Pro)
752
815
 
753
816
  A session can work across several repos. Attach another local repo you own
@@ -1228,7 +1291,7 @@ it's signed in without a plan that includes it, is reported to its supervisor.
1228
1291
  | Provider | Key login (environment variables) |
1229
1292
  |---|---|
1230
1293
  | `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` |
1294
+ | `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
1295
  | `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
1296
 
1234
1297
  Workers that run unattended should use key login where the CLI supports it.
@@ -1636,6 +1699,30 @@ marks the agent idle and delivers any queued message. Codex has no Stop hook, so
1636
1699
  an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
1637
1700
  going, with the same limit on reminders as Claude Code.
1638
1701
 
1702
+ ### Using your company's access
1703
+
1704
+ Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
1705
+ Foundry, a Team or Enterprise plan) and Codex, without handling keys:
1706
+
1707
+ - **Your org sets it up (brindle Team).** When your org's policy has an
1708
+ `agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
1709
+ in to the route the company chose and `brindle doctor` shows which identity
1710
+ each agent uses. It always wins over anything below.
1711
+ - **You set it up yourself (any plan).** When your org has no `agent_setup`,
1712
+ `brindle login` looks, read-only, at what this machine is already signed in
1713
+ to: Claude Code's managed settings (if your IT configures Claude Code,
1714
+ brindle just uses it), `claude auth status`, AWS SSO profiles in
1715
+ `~/.aws/config`, the active `gcloud` project, the active `az` subscription
1716
+ and `codex login status`. It never reads tokens or credential files. It
1717
+ proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
1718
+ the default is no, and with no terminal it is always no. A yes is saved as
1719
+ `"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
1720
+ policy's (`enforce` is never applied to it), and `brindle doctor` labels it
1721
+ as self-serve and says who is billed.
1722
+
1723
+ You must be authorized to use any company credentials you connect: brindle only
1724
+ signs in with the access you already have, it doesn't grant any.
1725
+
1639
1726
  ## Google Antigravity
1640
1727
 
1641
1728
  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.2"
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
@@ -1082,7 +1114,7 @@ def format_message(db: DB, body: str, sender_id: str | None) -> str:
1082
1114
  return body
1083
1115
  sender = db.get_agent(sender_id)
1084
1116
  who = f"{sender.profile} agent {sender_id}" if sender else f"agent {sender_id}"
1085
- return f"[Message from {who}. Reply with the brindle send_message tool, to_agent_id={sender_id}]\n\n{body}"
1117
+ return f"<details><summary>Message from {who}</summary>\n\nReply with the brindle send_message tool, to_agent_id={sender_id}\n\n{body}\n</details>"
1086
1118
 
1087
1119
 
1088
1120
  def message_lead(db: DB, agent: Agent, sender_id: str | None) -> str | None:
@@ -1129,8 +1161,8 @@ def render_unread(db: DB, messages: list) -> str:
1129
1161
  if m.sender_id:
1130
1162
  parts.append(m.body) # stored through format_message: sender and reply hint
1131
1163
  else:
1132
- parts.append(f"[Message from brindle]\n\n{m.body}")
1133
- return "\n\n---\n\n".join(parts)
1164
+ parts.append(f"<details><summary>Message from brindle</summary>\n\n{m.body}\n</details>")
1165
+ return "\n\n".join(parts)
1134
1166
 
1135
1167
 
1136
1168
  def send_message(db: DB, to_id: str, body: str, sender_id: str | None = None,
@@ -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)
@@ -1168,6 +1203,9 @@ def _send_message(db: DB, agent: Agent, body: str, sender_id: str | None) -> str
1168
1203
  raise AgentError(f"agent {agent.id} is not running")
1169
1204
  provider = get_provider(agent.provider)
1170
1205
  text = format_message(db, body, sender_id)
1206
+ if agent.provider == "antigravity":
1207
+ from brindle.antigravity import format_delivered_message
1208
+ text = format_delivered_message(text)
1171
1209
  if agent.headless:
1172
1210
  # Its runner starts the next turn with this, or the Stop hook hands it
1173
1211
  # over if a turn is still running.
@@ -1648,6 +1686,13 @@ def delegate(
1648
1686
  raise AgentError(str(e)) from e
1649
1687
  if plan_first is None:
1650
1688
  plan_first = load_repo_config(caller_ws.repo_root).plan_first
1689
+ from brindle import identity_lock
1690
+
1691
+ identity_lock.ensure_workspace(db, caller_ws)
1692
+ if isolate and branch: # naming a branch must not reuse another identity's worktree
1693
+ for other in db.find_workspaces(caller_ws.repo_root):
1694
+ if other.branch == branch:
1695
+ identity_lock.ensure_workspace(db, other)
1651
1696
  if isolate:
1652
1697
  caller_ws = workspaces.refresh_branch(db, caller_ws)
1653
1698
  # merge_into applies to the supervisor's workers; a worker's own