pulse-coding-agent 0.1.0__tar.gz → 0.1.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 (197) hide show
  1. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/.gitignore +0 -1
  2. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/CHANGELOG.md +11 -0
  3. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/PKG-INFO +24 -14
  4. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/README.md +23 -12
  5. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/pyproject.toml +1 -10
  6. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/__init__.py +1 -1
  7. pulse_coding_agent-0.1.1/src/pulse/_product_oauth.json +4 -0
  8. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/auth.py +134 -42
  9. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/cli.py +16 -15
  10. pulse_coding_agent-0.1.0/.env.example +0 -39
  11. pulse_coding_agent-0.1.0/ARCHITECTURE.md +0 -119
  12. pulse_coding_agent-0.1.0/OPERATIONS.md +0 -161
  13. pulse_coding_agent-0.1.0/agent.config.json +0 -25
  14. pulse_coding_agent-0.1.0/evals/provider-live-v1.json +0 -19
  15. pulse_coding_agent-0.1.0/evals/release-v1.json +0 -54
  16. pulse_coding_agent-0.1.0/scripts/__init__.py +0 -1
  17. pulse_coding_agent-0.1.0/scripts/finalize_sbom.py +0 -67
  18. pulse_coding_agent-0.1.0/scripts/generate_release_metadata.py +0 -88
  19. pulse_coding_agent-0.1.0/scripts/run_docker_release_tests.py +0 -69
  20. pulse_coding_agent-0.1.0/scripts/run_provider_e2e.py +0 -97
  21. pulse_coding_agent-0.1.0/scripts/run_release_evals.py +0 -87
  22. pulse_coding_agent-0.1.0/scripts/verify_release_artifacts.py +0 -191
  23. pulse_coding_agent-0.1.0/tests/conftest.py +0 -38
  24. pulse_coding_agent-0.1.0/tests/test_agent_manager.py +0 -173
  25. pulse_coding_agent-0.1.0/tests/test_architecture_contracts.py +0 -109
  26. pulse_coding_agent-0.1.0/tests/test_auth.py +0 -227
  27. pulse_coding_agent-0.1.0/tests/test_ci.py +0 -59
  28. pulse_coding_agent-0.1.0/tests/test_cli_entrypoint.py +0 -90
  29. pulse_coding_agent-0.1.0/tests/test_cli_flow.py +0 -139
  30. pulse_coding_agent-0.1.0/tests/test_cli_ui.py +0 -55
  31. pulse_coding_agent-0.1.0/tests/test_context.py +0 -458
  32. pulse_coding_agent-0.1.0/tests/test_conversation_manager.py +0 -287
  33. pulse_coding_agent-0.1.0/tests/test_core_agent.py +0 -56
  34. pulse_coding_agent-0.1.0/tests/test_correlation.py +0 -69
  35. pulse_coding_agent-0.1.0/tests/test_dag_and_impact.py +0 -88
  36. pulse_coding_agent-0.1.0/tests/test_docker_release_gate.py +0 -30
  37. pulse_coding_agent-0.1.0/tests/test_edits.py +0 -59
  38. pulse_coding_agent-0.1.0/tests/test_episodic_rules.py +0 -82
  39. pulse_coding_agent-0.1.0/tests/test_eval_harness.py +0 -90
  40. pulse_coding_agent-0.1.0/tests/test_execution_loop_failover.py +0 -65
  41. pulse_coding_agent-0.1.0/tests/test_git.py +0 -33
  42. pulse_coding_agent-0.1.0/tests/test_interactive.py +0 -158
  43. pulse_coding_agent-0.1.0/tests/test_mcp.py +0 -102
  44. pulse_coding_agent-0.1.0/tests/test_memory.py +0 -31
  45. pulse_coding_agent-0.1.0/tests/test_mutations.py +0 -40
  46. pulse_coding_agent-0.1.0/tests/test_orchestrator_safety.py +0 -43
  47. pulse_coding_agent-0.1.0/tests/test_patch.py +0 -96
  48. pulse_coding_agent-0.1.0/tests/test_planner.py +0 -86
  49. pulse_coding_agent-0.1.0/tests/test_production.py +0 -167
  50. pulse_coding_agent-0.1.0/tests/test_provider.py +0 -62
  51. pulse_coding_agent-0.1.0/tests/test_provider_e2e.py +0 -60
  52. pulse_coding_agent-0.1.0/tests/test_provider_manager.py +0 -142
  53. pulse_coding_agent-0.1.0/tests/test_provider_system.py +0 -58
  54. pulse_coding_agent-0.1.0/tests/test_public_cli.py +0 -274
  55. pulse_coding_agent-0.1.0/tests/test_reasoning.py +0 -222
  56. pulse_coding_agent-0.1.0/tests/test_release_artifacts.py +0 -75
  57. pulse_coding_agent-0.1.0/tests/test_release_evals.py +0 -57
  58. pulse_coding_agent-0.1.0/tests/test_release_metadata.py +0 -65
  59. pulse_coding_agent-0.1.0/tests/test_release_workflows.py +0 -42
  60. pulse_coding_agent-0.1.0/tests/test_remote_reconciliation.py +0 -143
  61. pulse_coding_agent-0.1.0/tests/test_repository.py +0 -50
  62. pulse_coding_agent-0.1.0/tests/test_rpc.py +0 -73
  63. pulse_coding_agent-0.1.0/tests/test_runtime.py +0 -49
  64. pulse_coding_agent-0.1.0/tests/test_sandbox.py +0 -30
  65. pulse_coding_agent-0.1.0/tests/test_sandbox_cow_adversarial.py +0 -112
  66. pulse_coding_agent-0.1.0/tests/test_sandbox_cow_crash.py +0 -227
  67. pulse_coding_agent-0.1.0/tests/test_sandbox_cow_hard_crash.py +0 -160
  68. pulse_coding_agent-0.1.0/tests/test_sandbox_lifecycle_recovery.py +0 -143
  69. pulse_coding_agent-0.1.0/tests/test_sandbox_phase1.py +0 -171
  70. pulse_coding_agent-0.1.0/tests/test_sandbox_phase2.py +0 -85
  71. pulse_coding_agent-0.1.0/tests/test_sandbox_phase3.py +0 -151
  72. pulse_coding_agent-0.1.0/tests/test_sandbox_phase4.py +0 -72
  73. pulse_coding_agent-0.1.0/tests/test_sandbox_phase5.py +0 -103
  74. pulse_coding_agent-0.1.0/tests/test_sandbox_phase7_network.py +0 -219
  75. pulse_coding_agent-0.1.0/tests/test_sandbox_phase8_secrets.py +0 -241
  76. pulse_coding_agent-0.1.0/tests/test_sandbox_r2_security.py +0 -486
  77. pulse_coding_agent-0.1.0/tests/test_sandbox_remote_auth.py +0 -60
  78. pulse_coding_agent-0.1.0/tests/test_sandbox_remote_security.py +0 -36
  79. pulse_coding_agent-0.1.0/tests/test_sandbox_resource_bombs.py +0 -78
  80. pulse_coding_agent-0.1.0/tests/test_sandbox_security.py +0 -849
  81. pulse_coding_agent-0.1.0/tests/test_sbom.py +0 -54
  82. pulse_coding_agent-0.1.0/tests/test_security_audit_regressions.py +0 -324
  83. pulse_coding_agent-0.1.0/tests/test_session_manager.py +0 -125
  84. pulse_coding_agent-0.1.0/tests/test_software_engineer.py +0 -140
  85. pulse_coding_agent-0.1.0/tests/test_storage.py +0 -111
  86. pulse_coding_agent-0.1.0/tests/test_streaming.py +0 -199
  87. pulse_coding_agent-0.1.0/tests/test_subprocesses.py +0 -80
  88. pulse_coding_agent-0.1.0/tests/test_task_execution_durability.py +0 -194
  89. pulse_coding_agent-0.1.0/tests/test_task_manager.py +0 -238
  90. pulse_coding_agent-0.1.0/tests/test_task_manager_concurrency.py +0 -280
  91. pulse_coding_agent-0.1.0/tests/test_task_manager_crash.py +0 -253
  92. pulse_coding_agent-0.1.0/tests/test_task_manager_lease_execution.py +0 -507
  93. pulse_coding_agent-0.1.0/tests/test_task_manager_migration.py +0 -53
  94. pulse_coding_agent-0.1.0/tests/test_telemetry.py +0 -88
  95. pulse_coding_agent-0.1.0/tests/test_tool_policy.py +0 -134
  96. pulse_coding_agent-0.1.0/tests/test_tool_registry.py +0 -52
  97. pulse_coding_agent-0.1.0/tests/test_verification.py +0 -57
  98. pulse_coding_agent-0.1.0/uv.lock +0 -1353
  99. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/PRIVACY.md +0 -0
  100. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/SECURITY.md +0 -0
  101. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/__main__.py +0 -0
  102. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/agent.py +0 -0
  103. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/agent_manager.py +0 -0
  104. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/audit.py +0 -0
  105. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/ci/github_client.py +0 -0
  106. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/ci/runner.py +0 -0
  107. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/cli_ui.py +0 -0
  108. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/config.py +0 -0
  109. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/context.py +0 -0
  110. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/conversations/__init__.py +0 -0
  111. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/conversations/manager.py +0 -0
  112. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/core/agent.py +0 -0
  113. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/core/planner.py +0 -0
  114. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/core/protocols.py +0 -0
  115. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/edits.py +0 -0
  116. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/episodic.py +0 -0
  117. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/eval/__init__.py +0 -0
  118. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/eval/trajectory_logger.py +0 -0
  119. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/eval/verifier.py +0 -0
  120. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/execution/__init__.py +0 -0
  121. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/execution/remote_task.py +0 -0
  122. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/git.py +0 -0
  123. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/interactive.py +0 -0
  124. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mcp/__init__.py +0 -0
  125. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mcp/client.py +0 -0
  126. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mcp/local_tools.py +0 -0
  127. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/memory.py +0 -0
  128. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mutations.py +0 -0
  129. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/orchestration/__init__.py +0 -0
  130. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/orchestration/orchestrator.py +0 -0
  131. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/patch.py +0 -0
  132. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/planner/__init__.py +0 -0
  133. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/planner/dag_planner.py +0 -0
  134. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/planner/execution_loop.py +0 -0
  135. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/production.py +0 -0
  136. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/provider.py +0 -0
  137. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/provider_keys.py +0 -0
  138. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/__init__.py +0 -0
  139. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/anthropic.py +0 -0
  140. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/base.py +0 -0
  141. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/deepseek.py +0 -0
  142. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/failover.py +0 -0
  143. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/gemini.py +0 -0
  144. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/groq.py +0 -0
  145. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/manager.py +0 -0
  146. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/openai.py +0 -0
  147. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/openrouter.py +0 -0
  148. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/py.typed +0 -0
  149. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/reasoning.py +0 -0
  150. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/refactor/__init__.py +0 -0
  151. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/refactor/impact_analyzer.py +0 -0
  152. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/repository.py +0 -0
  153. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/rpc.py +0 -0
  154. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/rule_synthesizer.py +0 -0
  155. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/runtime.py +0 -0
  156. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/safety/__init__.py +0 -0
  157. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/safety/safety_manager.py +0 -0
  158. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/SECURITY.md +0 -0
  159. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/__init__.py +0 -0
  160. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/api.py +0 -0
  161. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/audit.py +0 -0
  162. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/__init__.py +0 -0
  163. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/base.py +0 -0
  164. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/docker.py +0 -0
  165. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/host.py +0 -0
  166. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/remote.py +0 -0
  167. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/errors.py +0 -0
  168. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/filesystem.py +0 -0
  169. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/git_safe.py +0 -0
  170. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/lifecycle.py +0 -0
  171. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/network.py +0 -0
  172. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/path_validator.py +0 -0
  173. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/policy.py +0 -0
  174. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/process.py +0 -0
  175. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/project.py +0 -0
  176. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/python_safe.py +0 -0
  177. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/__init__.py +0 -0
  178. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/client.py +0 -0
  179. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/models.py +0 -0
  180. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/protocol.py +0 -0
  181. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/server.py +0 -0
  182. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/worker.py +0 -0
  183. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/resources.py +0 -0
  184. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/secrets.py +0 -0
  185. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/session_manager.py +0 -0
  186. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/software_engineer.py +0 -0
  187. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/storage.py +0 -0
  188. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/streaming.py +0 -0
  189. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/subprocesses.py +0 -0
  190. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/task_manager.py +0 -0
  191. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/telemetry/__init__.py +0 -0
  192. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/telemetry/cost_tracker.py +0 -0
  193. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/telemetry/logger.py +0 -0
  194. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/tool_policy.py +0 -0
  195. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/tool_registry.py +0 -0
  196. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/tools.py +0 -0
  197. {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/verification.py +0 -0
@@ -1,7 +1,6 @@
1
1
  # Secrets and machine-specific configuration
2
2
  .env
3
3
  .env.*
4
- !.env.example
5
4
  *.pem
6
5
  *.key
7
6
  *.p12
@@ -5,8 +5,15 @@ pre-1.0 APIs may change with release notes and migration guidance.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.1] - 2026-09-04
9
+
8
10
  ### Added
9
11
 
12
+ - Product-owned Google Desktop OAuth configuration is embedded in official
13
+ builds, so installed users can sign in without creating a `.env` file.
14
+ - Release verification now rejects missing/placeholder OAuth configuration,
15
+ confidential client secrets, developer-only source content, and local home
16
+ directory paths in published artifacts.
10
17
  - Post-login BYOK onboarding now guides provider and model selection before
11
18
  collecting the selected provider key through hidden terminal input.
12
19
  - Provider keys are stored in workspace-scoped native OS credential-vault
@@ -14,6 +21,10 @@ pre-1.0 APIs may change with release notes and migration guidance.
14
21
 
15
22
  ### Changed
16
23
 
24
+ - Google login uses PKCE with a dynamically allocated `127.0.0.1` callback port
25
+ and no confidential client secret in the distributed application.
26
+ - Source distributions contain only the files needed to build and understand
27
+ the product, rather than repository tests, scripts, evals, and local templates.
17
28
  - Successful key set or rotation migrates that provider away from a legacy
18
29
  plaintext workspace `.env` entry without ever displaying the secret.
19
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pulse-coding-agent
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Permissioned coding-agent CLI with durable planning and isolated execution.
5
5
  Project-URL: Homepage, https://github.com/Roastedpotato21/pulse
6
6
  Project-URL: Repository, https://github.com/Roastedpotato21/pulse
@@ -23,7 +23,6 @@ Requires-Dist: httpx==0.28.1
23
23
  Requires-Dist: keyring==25.7.0
24
24
  Requires-Dist: openai==2.44.0
25
25
  Requires-Dist: prompt-toolkit==3.0.52
26
- Requires-Dist: python-dotenv==1.2.2
27
26
  Requires-Dist: rich==15.0.0
28
27
  Requires-Dist: typer==0.26.8
29
28
  Requires-Dist: websockets==16.1
@@ -36,7 +35,7 @@ It combines repository-aware planning, durable task state, explicit mutation
36
35
  controls, sandboxed command execution, provider routing, audit trails, and cost
37
36
  tracking in one Python package.
38
37
 
39
- > **Public beta — 0.1.0:** Pulse is supported for controlled, local,
38
+ > **Public beta — 0.1.1:** Pulse is supported for controlled, local,
40
39
  > single-user use. The remote worker and VS Code extension are included for
41
40
  > evaluation and development, but they are not supported as multi-tenant or
42
41
  > unattended production services.
@@ -50,14 +49,23 @@ tracking in one Python package.
50
49
 
51
50
  ## Install
52
51
 
53
- Install the first beta from PyPI:
52
+ Install the first beta from PyPI with `pip`:
54
53
 
55
54
  ```bash
56
- uv tool install pulse-coding-agent==0.1.0
55
+ python -m pip install pulse-coding-agent==0.1.1
57
56
  ```
58
57
 
59
- Alternatively, use `pipx install pulse-coding-agent==0.1.0` or install it in a
60
- dedicated virtual environment with `pip`.
58
+ The PyPI distribution name is `pulse-coding-agent`; the Python import and
59
+ installed command are both `pulse`. Do not run `pip install pulse`: that name
60
+ belongs to an unrelated WSGI package published in 2009.
61
+
62
+ For an isolated command-line installation, use `uv` or `pipx`:
63
+
64
+ ```bash
65
+ uv tool install pulse-coding-agent==0.1.1
66
+ ```
67
+
68
+ Alternatively, use `pipx install pulse-coding-agent==0.1.1`.
61
69
 
62
70
  ## Quick start
63
71
 
@@ -71,9 +79,10 @@ pulse ask "Explain this repository and identify the highest-risk missing test"
71
79
 
72
80
  After Google login, Pulse guides provider selection, model selection, and hidden
73
81
  BYOK entry. New provider keys are stored in the native OS credential vault.
74
- Credentials can also be supplied through environment variables such as `OPENAI_API_KEY`,
75
- `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or `OPENROUTER_API_KEY`. Do not commit a
76
- populated `.env` file.
82
+ The official distribution already contains Pulse's public Google Desktop OAuth
83
+ identity; installed users do not create a `.env` file or provide OAuth client
84
+ credentials. Provider API keys are collected through hidden terminal input and
85
+ are not written to the project.
77
86
 
78
87
  To run commands without installing Docker on the client workstation, configure
79
88
  a remote worker:
@@ -153,12 +162,13 @@ prompt with hidden input; provider keys are never accepted as command-line
153
162
  arguments or printed back to the terminal. New keys are stored in a
154
163
  workspace-scoped entry in the native OS credential vault. Existing `.env` keys
155
164
  remain readable for compatibility and are removed for that provider after a
156
- successful vault-backed set or rotation. Environment variables and external
157
- secret managers remain supported as fallback sources.
165
+ successful vault-backed set or rotation. Pulse never creates a `.env` file.
166
+ Process environment variables and external secret managers remain supported as
167
+ operator-managed fallback sources.
158
168
 
159
169
  ## Supported boundary
160
170
 
161
- The 0.1.0 beta supports the local CLI and loopback RPC server for one trusted
171
+ The 0.1.1 beta supports the local CLI and loopback RPC server for one trusted
162
172
  user on one workstation. Pulse does not claim a security boundary between
163
173
  mutually untrusted tenants. Do not expose `pulse-rpc` or `pulse-remote` directly
164
174
  to the public internet.
@@ -190,7 +200,7 @@ uv run ruff check src tests scripts
190
200
  uv run mypy
191
201
  uv run pytest tests/ -k "not sandbox" --cov=pulse --cov-report=term-missing --cov-fail-under=54
192
202
  uv build
193
- uv run python scripts/verify_release_artifacts.py dist --expected-version 0.1.0
203
+ uv run python scripts/verify_release_artifacts.py dist --expected-version 0.1.1
194
204
  ```
195
205
 
196
206
  Docker security tests require a working Docker daemon and are enforced by the
@@ -5,7 +5,7 @@ It combines repository-aware planning, durable task state, explicit mutation
5
5
  controls, sandboxed command execution, provider routing, audit trails, and cost
6
6
  tracking in one Python package.
7
7
 
8
- > **Public beta — 0.1.0:** Pulse is supported for controlled, local,
8
+ > **Public beta — 0.1.1:** Pulse is supported for controlled, local,
9
9
  > single-user use. The remote worker and VS Code extension are included for
10
10
  > evaluation and development, but they are not supported as multi-tenant or
11
11
  > unattended production services.
@@ -19,14 +19,23 @@ tracking in one Python package.
19
19
 
20
20
  ## Install
21
21
 
22
- Install the first beta from PyPI:
22
+ Install the first beta from PyPI with `pip`:
23
23
 
24
24
  ```bash
25
- uv tool install pulse-coding-agent==0.1.0
25
+ python -m pip install pulse-coding-agent==0.1.1
26
26
  ```
27
27
 
28
- Alternatively, use `pipx install pulse-coding-agent==0.1.0` or install it in a
29
- dedicated virtual environment with `pip`.
28
+ The PyPI distribution name is `pulse-coding-agent`; the Python import and
29
+ installed command are both `pulse`. Do not run `pip install pulse`: that name
30
+ belongs to an unrelated WSGI package published in 2009.
31
+
32
+ For an isolated command-line installation, use `uv` or `pipx`:
33
+
34
+ ```bash
35
+ uv tool install pulse-coding-agent==0.1.1
36
+ ```
37
+
38
+ Alternatively, use `pipx install pulse-coding-agent==0.1.1`.
30
39
 
31
40
  ## Quick start
32
41
 
@@ -40,9 +49,10 @@ pulse ask "Explain this repository and identify the highest-risk missing test"
40
49
 
41
50
  After Google login, Pulse guides provider selection, model selection, and hidden
42
51
  BYOK entry. New provider keys are stored in the native OS credential vault.
43
- Credentials can also be supplied through environment variables such as `OPENAI_API_KEY`,
44
- `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or `OPENROUTER_API_KEY`. Do not commit a
45
- populated `.env` file.
52
+ The official distribution already contains Pulse's public Google Desktop OAuth
53
+ identity; installed users do not create a `.env` file or provide OAuth client
54
+ credentials. Provider API keys are collected through hidden terminal input and
55
+ are not written to the project.
46
56
 
47
57
  To run commands without installing Docker on the client workstation, configure
48
58
  a remote worker:
@@ -122,12 +132,13 @@ prompt with hidden input; provider keys are never accepted as command-line
122
132
  arguments or printed back to the terminal. New keys are stored in a
123
133
  workspace-scoped entry in the native OS credential vault. Existing `.env` keys
124
134
  remain readable for compatibility and are removed for that provider after a
125
- successful vault-backed set or rotation. Environment variables and external
126
- secret managers remain supported as fallback sources.
135
+ successful vault-backed set or rotation. Pulse never creates a `.env` file.
136
+ Process environment variables and external secret managers remain supported as
137
+ operator-managed fallback sources.
127
138
 
128
139
  ## Supported boundary
129
140
 
130
- The 0.1.0 beta supports the local CLI and loopback RPC server for one trusted
141
+ The 0.1.1 beta supports the local CLI and loopback RPC server for one trusted
131
142
  user on one workstation. Pulse does not claim a security boundary between
132
143
  mutually untrusted tenants. Do not expose `pulse-rpc` or `pulse-remote` directly
133
144
  to the public internet.
@@ -159,7 +170,7 @@ uv run ruff check src tests scripts
159
170
  uv run mypy
160
171
  uv run pytest tests/ -k "not sandbox" --cov=pulse --cov-report=term-missing --cov-fail-under=54
161
172
  uv build
162
- uv run python scripts/verify_release_artifacts.py dist --expected-version 0.1.0
173
+ uv run python scripts/verify_release_artifacts.py dist --expected-version 0.1.1
163
174
  ```
164
175
 
165
176
  Docker security tests require a working Docker daemon and are enforced by the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pulse-coding-agent"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Permissioned coding-agent CLI with durable planning and isolated execution."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.11,<3.14"
@@ -26,7 +26,6 @@ dependencies = [
26
26
  "keyring==25.7.0",
27
27
  "openai==2.44.0",
28
28
  "prompt-toolkit==3.0.52",
29
- "python-dotenv==1.2.2",
30
29
  "rich==15.0.0",
31
30
  "typer==0.26.8",
32
31
  "websockets==16.1",
@@ -53,19 +52,11 @@ packages = ["src/pulse"]
53
52
  [tool.hatch.build.targets.sdist]
54
53
  include = [
55
54
  "/src",
56
- "/scripts",
57
- "/tests",
58
- "/evals",
59
55
  "/README.md",
60
- "/ARCHITECTURE.md",
61
- "/OPERATIONS.md",
62
56
  "/SECURITY.md",
63
57
  "/PRIVACY.md",
64
58
  "/CHANGELOG.md",
65
- "/agent.config.json",
66
- "/.env.example",
67
59
  "/pyproject.toml",
68
- "/uv.lock",
69
60
  ]
70
61
 
71
62
  [tool.pytest.ini_options]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.1.1"
@@ -0,0 +1,4 @@
1
+ {
2
+ "client_id": "not-configured",
3
+ "redirect_uri": "http://127.0.0.1"
4
+ }
@@ -11,9 +11,11 @@ import base64
11
11
  import hashlib
12
12
  import hmac
13
13
  import http.server
14
+ import importlib.resources
14
15
  import json
15
16
  import logging
16
17
  import os
18
+ import re
17
19
  import secrets
18
20
  import socketserver
19
21
  import time
@@ -34,6 +36,14 @@ logger = logging.getLogger(__name__)
34
36
 
35
37
  KEYRING_SERVICE_NAME = "pulse-cli"
36
38
  KEYRING_ACCOUNT_NAME = "current_session"
39
+ PRODUCT_OAUTH_RESOURCE = "_product_oauth.json"
40
+ PRODUCT_CLIENT_ID_ENV = "PULSE_GOOGLE_CLIENT_ID"
41
+ PRODUCT_REDIRECT_URI_ENV = "PULSE_GOOGLE_REDIRECT_URI"
42
+ DEFAULT_REDIRECT_URI = "http://127.0.0.1"
43
+ _GOOGLE_CLIENT_ID = re.compile(
44
+ r"^[0-9]+-[A-Za-z0-9_-]+\.apps\.googleusercontent\.com$"
45
+ )
46
+ _UNCONFIGURED_CLIENT_IDS = {"", "not-configured", "placeholder", "replace_me"}
37
47
 
38
48
 
39
49
  # --- Exceptions ---
@@ -42,6 +52,10 @@ class AuthError(Exception):
42
52
  """Base exception for authentication errors."""
43
53
 
44
54
 
55
+ class AuthConfigurationError(AuthError):
56
+ """Raised when an installation has no valid product OAuth identity."""
57
+
58
+
45
59
  class AuthTimeoutError(AuthError):
46
60
  """Raised when authentication times out waiting for browser callback."""
47
61
 
@@ -264,7 +278,7 @@ class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
264
278
  self._send_response_page(
265
279
  status=200,
266
280
  title="Authentication Successful",
267
- message="Successfully authenticated with Pulse! You can close this window and return to your terminal.",
281
+ message="Successfully authenticated with Pulse. You can close this window and return to your terminal.",
268
282
  is_success=True,
269
283
  )
270
284
  return
@@ -329,44 +343,82 @@ class SingleRequestHTTPServer(socketserver.TCPServer):
329
343
 
330
344
  # --- OAuth Client Helper ---
331
345
 
332
- def get_google_config() -> dict[str, str]:
333
- """Retrieve only the OAuth values Pulse recognizes, without mutating the process."""
334
- from pulse.config import load_env_file
346
+ def _packaged_google_config() -> dict[str, str]:
347
+ """Load the public OAuth identity embedded in the installed distribution."""
348
+ try:
349
+ resource = importlib.resources.files("pulse").joinpath(PRODUCT_OAUTH_RESOURCE)
350
+ raw = resource.read_text(encoding="utf-8")
351
+ if len(raw.encode("utf-8")) > 4096:
352
+ raise AuthConfigurationError("The packaged Google sign-in configuration is invalid.")
353
+ parsed = json.loads(raw)
354
+ except AuthConfigurationError:
355
+ raise
356
+ except (FileNotFoundError, ModuleNotFoundError, OSError, TypeError, json.JSONDecodeError):
357
+ return {}
335
358
 
359
+ if not isinstance(parsed, dict):
360
+ raise AuthConfigurationError("The packaged Google sign-in configuration is invalid.")
361
+ return {
362
+ "client_id": str(parsed.get("client_id", "")).strip(),
363
+ "redirect_uri": str(parsed.get("redirect_uri", DEFAULT_REDIRECT_URI)).strip(),
364
+ }
365
+
366
+
367
+ def _validated_redirect_uri(value: str) -> str:
336
368
  try:
337
- file_values = load_env_file(Path.cwd() / ".env")
338
- except (OSError, UnicodeError, ValueError):
339
- file_values = {}
340
- client_id = os.environ.get("GOOGLE_CLIENT_ID") or file_values.get("GOOGLE_CLIENT_ID", "")
341
- client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") or file_values.get(
342
- "GOOGLE_CLIENT_SECRET", ""
343
- )
344
- redirect_uri = os.environ.get("GOOGLE_REDIRECT_URI") or file_values.get(
345
- "GOOGLE_REDIRECT_URI", "http://localhost:8080"
346
- )
347
- parsed = urllib.parse.urlparse(redirect_uri)
369
+ parsed = urllib.parse.urlparse(value)
370
+ port = parsed.port
371
+ except ValueError as error:
372
+ raise AuthConfigurationError(
373
+ "The Google sign-in callback configuration in this installation is invalid."
374
+ ) from error
348
375
  if (
349
376
  parsed.scheme != "http"
350
- or parsed.hostname not in {"localhost", "127.0.0.1", "::1"}
377
+ or parsed.hostname != "127.0.0.1"
351
378
  or parsed.username is not None
352
379
  or parsed.password is not None
353
380
  or parsed.query
354
381
  or parsed.fragment
382
+ or (port is not None and not 1 <= port <= 65535)
383
+ ):
384
+ raise AuthConfigurationError(
385
+ "The Google sign-in callback configuration in this installation is invalid."
386
+ )
387
+ return value
388
+
389
+
390
+ def get_google_config() -> dict[str, str]:
391
+ """Resolve the product-owned public OAuth identity without reading workspace files."""
392
+ packaged = _packaged_google_config()
393
+ client_id = os.environ.get(PRODUCT_CLIENT_ID_ENV, "").strip() or packaged.get(
394
+ "client_id", ""
395
+ )
396
+ redirect_uri = os.environ.get(PRODUCT_REDIRECT_URI_ENV, "").strip() or packaged.get(
397
+ "redirect_uri", DEFAULT_REDIRECT_URI
398
+ )
399
+
400
+ if (
401
+ client_id.lower() in _UNCONFIGURED_CLIENT_IDS
402
+ or not _GOOGLE_CLIENT_ID.fullmatch(client_id)
355
403
  ):
356
- raise ValueError("GOOGLE_REDIRECT_URI must be an HTTP loopback URL without credentials or query data.")
404
+ raise AuthConfigurationError(
405
+ "This Pulse installation does not include a valid Google sign-in client."
406
+ )
357
407
 
358
408
  return {
359
- "client_id": client_id or "",
360
- "client_secret": client_secret or "",
361
- "redirect_uri": redirect_uri or "http://localhost:8080",
409
+ "client_id": client_id,
410
+ "redirect_uri": _validated_redirect_uri(redirect_uri or DEFAULT_REDIRECT_URI),
362
411
  }
363
412
 
364
413
 
365
- def build_authorization_url(state: str, code_challenge: str) -> str:
366
- config = get_google_config()
414
+ def build_authorization_url(
415
+ state: str,
416
+ code_challenge: str,
417
+ *,
418
+ config: dict[str, str] | None = None,
419
+ ) -> str:
420
+ config = config or get_google_config()
367
421
  client_id = config["client_id"]
368
- if not client_id or client_id == "replace_me":
369
- raise ValueError("GOOGLE_CLIENT_ID environment variable is not set or contains default 'replace_me'")
370
422
 
371
423
  params = {
372
424
  "client_id": client_id,
@@ -382,12 +434,16 @@ def build_authorization_url(state: str, code_challenge: str) -> str:
382
434
  return f"https://accounts.google.com/o/oauth2/v2/auth?{urllib.parse.urlencode(params)}"
383
435
 
384
436
 
385
- def exchange_code_for_tokens(code: str, code_verifier: str) -> tuple[TokenSet, UserProfile]:
386
- config = get_google_config()
437
+ def exchange_code_for_tokens(
438
+ code: str,
439
+ code_verifier: str,
440
+ *,
441
+ config: dict[str, str] | None = None,
442
+ ) -> tuple[TokenSet, UserProfile]:
443
+ config = config or get_google_config()
387
444
  data = urllib.parse.urlencode({
388
445
  "code": code,
389
446
  "client_id": config["client_id"],
390
- "client_secret": config["client_secret"],
391
447
  "redirect_uri": config["redirect_uri"],
392
448
  "grant_type": "authorization_code",
393
449
  "code_verifier": code_verifier,
@@ -404,14 +460,28 @@ def exchange_code_for_tokens(code: str, code_verifier: str) -> tuple[TokenSet, U
404
460
  token_res = json.loads(resp.read().decode("utf-8"))
405
461
  except urllib.error.URLError as error:
406
462
  raise AuthError("Failed to exchange the authorization code with Google.") from error
463
+ except (json.JSONDecodeError, OSError, UnicodeError) as error:
464
+ raise AuthError("Google returned an invalid token response.") from error
465
+
466
+ if not isinstance(token_res, dict):
467
+ raise AuthError("Google returned an invalid token response.")
407
468
 
408
469
  access_token = token_res.get("access_token")
409
- if not access_token:
470
+ if not isinstance(access_token, str) or not access_token:
410
471
  raise AuthError("Token endpoint returned no access_token.")
411
472
 
412
473
  refresh_token = token_res.get("refresh_token")
413
474
  id_token = token_res.get("id_token")
414
- expires_in = int(token_res.get("expires_in", 3600))
475
+ if refresh_token is not None and not isinstance(refresh_token, str):
476
+ raise AuthError("Google returned an invalid refresh token.")
477
+ if id_token is not None and not isinstance(id_token, str):
478
+ raise AuthError("Google returned an invalid identity token.")
479
+ try:
480
+ expires_in = int(token_res.get("expires_in", 3600))
481
+ except (TypeError, ValueError) as error:
482
+ raise AuthError("Google returned an invalid token lifetime.") from error
483
+ if expires_in <= 0:
484
+ raise AuthError("Google returned an invalid token lifetime.")
415
485
  expires_at = time.time() + expires_in
416
486
 
417
487
  token_set = TokenSet(
@@ -534,7 +604,6 @@ class AuthenticationManager:
534
604
  config = get_google_config()
535
605
  data = urllib.parse.urlencode({
536
606
  "client_id": config["client_id"],
537
- "client_secret": config["client_secret"],
538
607
  "refresh_token": tokens.refresh_token,
539
608
  "grant_type": "refresh_token",
540
609
  }).encode("utf-8")
@@ -548,17 +617,28 @@ class AuthenticationManager:
548
617
  try:
549
618
  with urllib.request.urlopen(req, timeout=15) as resp:
550
619
  token_res = json.loads(resp.read().decode("utf-8"))
551
- except (urllib.error.URLError, json.JSONDecodeError, OSError):
620
+ except (urllib.error.URLError, json.JSONDecodeError, OSError, UnicodeError):
621
+ return False
622
+ if not isinstance(token_res, dict):
552
623
  return False
553
624
 
554
625
  new_access_token = token_res.get("access_token")
555
- if not new_access_token:
626
+ if not isinstance(new_access_token, str) or not new_access_token:
556
627
  return False
557
628
 
558
- expires_in = int(token_res.get("expires_in", 3600))
629
+ try:
630
+ expires_in = int(token_res.get("expires_in", 3600))
631
+ except (TypeError, ValueError):
632
+ return False
633
+ if expires_in <= 0:
634
+ return False
559
635
  expires_at = time.time() + expires_in
560
636
  new_refresh_token = token_res.get("refresh_token") or tokens.refresh_token
561
637
  new_id_token = token_res.get("id_token") or tokens.id_token
638
+ if not isinstance(new_refresh_token, (str, type(None))) or not isinstance(
639
+ new_id_token, (str, type(None))
640
+ ):
641
+ return False
562
642
 
563
643
  updated_tokens = TokenSet(
564
644
  access_token=new_access_token,
@@ -622,20 +702,26 @@ def login(timeout_seconds: int = 120) -> UserProfile | None:
622
702
 
623
703
  state = generate_state()
624
704
  code_verifier, code_challenge = generate_pkce_pair()
625
- auth_url = build_authorization_url(state, code_challenge)
626
-
627
705
  config = get_google_config()
628
- parsed_redirect = urllib.parse.urlparse(config.get("redirect_uri", "http://localhost:8080"))
629
- port = parsed_redirect.port or 8080
706
+ parsed_redirect = urllib.parse.urlparse(config["redirect_uri"])
707
+ requested_port = parsed_redirect.port or 0
630
708
 
631
709
  OAuthCallbackHandler.received_code = None
632
710
  OAuthCallbackHandler.received_state = None
633
711
  OAuthCallbackHandler.received_error = None
634
712
 
635
713
  try:
636
- server = SingleRequestHTTPServer(("localhost", port), OAuthCallbackHandler)
714
+ server = SingleRequestHTTPServer(("127.0.0.1", requested_port), OAuthCallbackHandler)
637
715
  except OSError as e:
638
- raise AuthError(f"Could not start local HTTP server on port {port}: {e}") from e
716
+ port_label = str(requested_port) if requested_port else "an available port"
717
+ raise AuthError(f"Could not start the local sign-in listener on {port_label}.") from e
718
+
719
+ bound_port = int(server.server_address[1])
720
+ redirect_uri = urllib.parse.urlunparse(
721
+ parsed_redirect._replace(netloc=f"127.0.0.1:{bound_port}")
722
+ )
723
+ flow_config = {**config, "redirect_uri": redirect_uri}
724
+ auth_url = build_authorization_url(state, code_challenge, config=flow_config)
639
725
 
640
726
  server.timeout = timeout_seconds
641
727
 
@@ -650,8 +736,10 @@ def login(timeout_seconds: int = 120) -> UserProfile | None:
650
736
  except (webbrowser.Error, OSError) as err:
651
737
  logger.debug(f"Webbrowser open failed: {err}")
652
738
 
653
- server.handle_request()
654
- server.server_close()
739
+ try:
740
+ server.handle_request()
741
+ finally:
742
+ server.server_close()
655
743
 
656
744
  if OAuthCallbackHandler.received_error:
657
745
  raise UserCancelledError("Authentication was cancelled in browser.")
@@ -665,6 +753,10 @@ def login(timeout_seconds: int = 120) -> UserProfile | None:
665
753
  if cb_state != state:
666
754
  raise StateMismatchError("OAuth state verification failed. Possible CSRF attack.")
667
755
 
668
- tokens, user_profile = exchange_code_for_tokens(code, code_verifier)
756
+ tokens, user_profile = exchange_code_for_tokens(
757
+ code,
758
+ code_verifier,
759
+ config=flow_config,
760
+ )
669
761
  _token_store.store_session(user_profile, tokens)
670
762
  return user_profile
@@ -4,16 +4,17 @@ import argparse
4
4
  import asyncio
5
5
  import getpass
6
6
  import io
7
- import json
8
- import shutil
9
- import sys
10
- import warnings
7
+ import json
8
+ import shutil
9
+ import sys
10
+ import warnings
11
11
  from pathlib import Path
12
12
 
13
13
  from rich.table import Table
14
14
 
15
15
  from pulse import __version__
16
16
  from pulse.auth import (
17
+ AuthConfigurationError,
17
18
  AuthenticationManager,
18
19
  AuthError,
19
20
  AuthTimeoutError,
@@ -534,19 +535,19 @@ def _handle_login_command() -> bool:
534
535
  except AuthTimeoutError:
535
536
  print_error("Authentication timed out waiting for browser callback.")
536
537
  sys.exit(1)
537
- except StateMismatchError:
538
- print_error("Authentication state mismatch. Possible security issue.")
539
- sys.exit(1)
540
- except AuthError as e:
541
- print_error(f"Authentication failed: {e}")
542
- sys.exit(1)
543
- except ValueError as e:
544
- print_error(f"Configuration error: {e}")
545
- print_info(
546
- "Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in your .env file.\n"
547
- "See .env.example and the README for setup instructions."
538
+ except StateMismatchError:
539
+ print_error("Authentication state mismatch. Possible security issue.")
540
+ sys.exit(1)
541
+ except AuthConfigurationError:
542
+ print_error("Google sign-in is unavailable in this installation.")
543
+ print_info(
544
+ "Install the latest official pulse-coding-agent release. "
545
+ "Product authentication never requires a workspace .env file."
548
546
  )
549
547
  sys.exit(1)
548
+ except AuthError as e:
549
+ print_error(f"Authentication failed: {e}")
550
+ sys.exit(1)
550
551
 
551
552
 
552
553
  def _run_provider_onboarding(workspace: Path) -> bool:
@@ -1,39 +0,0 @@
1
- OPENROUTER_API_KEY=replace_me
2
- GEMINI_API_KEY=replace_me
3
- OPENAI_API_KEY=replace_me
4
-
5
- AGENT_PROVIDER=openrouter
6
- AGENT_FALLBACK_PROVIDER=gemini
7
- AGENT_MODEL=qwen/qwen3-coder
8
- AGENT_MAX_TOKENS=8192
9
-
10
- PULSE_MAX_SESSION_TOKENS=100000
11
- PULSE_MAX_SESSION_COST=1.00
12
-
13
- # Google OAuth 2.0 ("Continue with Google")
14
- # Create credentials at: https://console.cloud.google.com/apis/credentials
15
- # Application type: Web application
16
- # Authorized redirect URIs: http://localhost:8080
17
- GOOGLE_CLIENT_ID=replace_me
18
- GOOGLE_CLIENT_SECRET=replace_me
19
- GOOGLE_REDIRECT_URI=http://localhost:8080
20
-
21
- # Remote-sandbox client. Uncomment both values to prefer remote execution;
22
- # local Docker/Podman is the secure fallback when the endpoint is unavailable.
23
- # PULSE_REMOTE_URL=wss://sandbox.example.com
24
- # PULSE_REMOTE_TOKEN=replace_with_a_random_32_character_token
25
- # PULSE_TLS_CERT=/absolute/path/to/client.crt
26
- # PULSE_TLS_KEY=/absolute/path/to/client.key
27
- # PULSE_TLS_CA=/absolute/path/to/ca.crt
28
-
29
- # Dedicated remote-worker server. Use a random token with at least 32 characters.
30
- # Non-loopback workers also require all three mTLS paths.
31
- # PULSE_REMOTE_HOST=127.0.0.1
32
- # PULSE_REMOTE_PORT=8080
33
- # PULSE_REMOTE_TOKEN=replace_with_a_random_32_character_token
34
- # PULSE_REMOTE_WORKSPACE_ROOT=/var/lib/pulse/workspaces
35
- # PULSE_REMOTE_DB=/var/lib/pulse/executions.sqlite3
36
- # PULSE_REMOTE_MAX_CONCURRENCY=10
37
- # PULSE_REMOTE_RETENTION_HOURS=24
38
- # The worker process uses the same TLS variable names with its server
39
- # certificate, key, and trusted client CA.