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.
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/.gitignore +0 -1
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/CHANGELOG.md +11 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/PKG-INFO +24 -14
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/README.md +23 -12
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/pyproject.toml +1 -10
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/__init__.py +1 -1
- pulse_coding_agent-0.1.1/src/pulse/_product_oauth.json +4 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/auth.py +134 -42
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/cli.py +16 -15
- pulse_coding_agent-0.1.0/.env.example +0 -39
- pulse_coding_agent-0.1.0/ARCHITECTURE.md +0 -119
- pulse_coding_agent-0.1.0/OPERATIONS.md +0 -161
- pulse_coding_agent-0.1.0/agent.config.json +0 -25
- pulse_coding_agent-0.1.0/evals/provider-live-v1.json +0 -19
- pulse_coding_agent-0.1.0/evals/release-v1.json +0 -54
- pulse_coding_agent-0.1.0/scripts/__init__.py +0 -1
- pulse_coding_agent-0.1.0/scripts/finalize_sbom.py +0 -67
- pulse_coding_agent-0.1.0/scripts/generate_release_metadata.py +0 -88
- pulse_coding_agent-0.1.0/scripts/run_docker_release_tests.py +0 -69
- pulse_coding_agent-0.1.0/scripts/run_provider_e2e.py +0 -97
- pulse_coding_agent-0.1.0/scripts/run_release_evals.py +0 -87
- pulse_coding_agent-0.1.0/scripts/verify_release_artifacts.py +0 -191
- pulse_coding_agent-0.1.0/tests/conftest.py +0 -38
- pulse_coding_agent-0.1.0/tests/test_agent_manager.py +0 -173
- pulse_coding_agent-0.1.0/tests/test_architecture_contracts.py +0 -109
- pulse_coding_agent-0.1.0/tests/test_auth.py +0 -227
- pulse_coding_agent-0.1.0/tests/test_ci.py +0 -59
- pulse_coding_agent-0.1.0/tests/test_cli_entrypoint.py +0 -90
- pulse_coding_agent-0.1.0/tests/test_cli_flow.py +0 -139
- pulse_coding_agent-0.1.0/tests/test_cli_ui.py +0 -55
- pulse_coding_agent-0.1.0/tests/test_context.py +0 -458
- pulse_coding_agent-0.1.0/tests/test_conversation_manager.py +0 -287
- pulse_coding_agent-0.1.0/tests/test_core_agent.py +0 -56
- pulse_coding_agent-0.1.0/tests/test_correlation.py +0 -69
- pulse_coding_agent-0.1.0/tests/test_dag_and_impact.py +0 -88
- pulse_coding_agent-0.1.0/tests/test_docker_release_gate.py +0 -30
- pulse_coding_agent-0.1.0/tests/test_edits.py +0 -59
- pulse_coding_agent-0.1.0/tests/test_episodic_rules.py +0 -82
- pulse_coding_agent-0.1.0/tests/test_eval_harness.py +0 -90
- pulse_coding_agent-0.1.0/tests/test_execution_loop_failover.py +0 -65
- pulse_coding_agent-0.1.0/tests/test_git.py +0 -33
- pulse_coding_agent-0.1.0/tests/test_interactive.py +0 -158
- pulse_coding_agent-0.1.0/tests/test_mcp.py +0 -102
- pulse_coding_agent-0.1.0/tests/test_memory.py +0 -31
- pulse_coding_agent-0.1.0/tests/test_mutations.py +0 -40
- pulse_coding_agent-0.1.0/tests/test_orchestrator_safety.py +0 -43
- pulse_coding_agent-0.1.0/tests/test_patch.py +0 -96
- pulse_coding_agent-0.1.0/tests/test_planner.py +0 -86
- pulse_coding_agent-0.1.0/tests/test_production.py +0 -167
- pulse_coding_agent-0.1.0/tests/test_provider.py +0 -62
- pulse_coding_agent-0.1.0/tests/test_provider_e2e.py +0 -60
- pulse_coding_agent-0.1.0/tests/test_provider_manager.py +0 -142
- pulse_coding_agent-0.1.0/tests/test_provider_system.py +0 -58
- pulse_coding_agent-0.1.0/tests/test_public_cli.py +0 -274
- pulse_coding_agent-0.1.0/tests/test_reasoning.py +0 -222
- pulse_coding_agent-0.1.0/tests/test_release_artifacts.py +0 -75
- pulse_coding_agent-0.1.0/tests/test_release_evals.py +0 -57
- pulse_coding_agent-0.1.0/tests/test_release_metadata.py +0 -65
- pulse_coding_agent-0.1.0/tests/test_release_workflows.py +0 -42
- pulse_coding_agent-0.1.0/tests/test_remote_reconciliation.py +0 -143
- pulse_coding_agent-0.1.0/tests/test_repository.py +0 -50
- pulse_coding_agent-0.1.0/tests/test_rpc.py +0 -73
- pulse_coding_agent-0.1.0/tests/test_runtime.py +0 -49
- pulse_coding_agent-0.1.0/tests/test_sandbox.py +0 -30
- pulse_coding_agent-0.1.0/tests/test_sandbox_cow_adversarial.py +0 -112
- pulse_coding_agent-0.1.0/tests/test_sandbox_cow_crash.py +0 -227
- pulse_coding_agent-0.1.0/tests/test_sandbox_cow_hard_crash.py +0 -160
- pulse_coding_agent-0.1.0/tests/test_sandbox_lifecycle_recovery.py +0 -143
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase1.py +0 -171
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase2.py +0 -85
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase3.py +0 -151
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase4.py +0 -72
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase5.py +0 -103
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase7_network.py +0 -219
- pulse_coding_agent-0.1.0/tests/test_sandbox_phase8_secrets.py +0 -241
- pulse_coding_agent-0.1.0/tests/test_sandbox_r2_security.py +0 -486
- pulse_coding_agent-0.1.0/tests/test_sandbox_remote_auth.py +0 -60
- pulse_coding_agent-0.1.0/tests/test_sandbox_remote_security.py +0 -36
- pulse_coding_agent-0.1.0/tests/test_sandbox_resource_bombs.py +0 -78
- pulse_coding_agent-0.1.0/tests/test_sandbox_security.py +0 -849
- pulse_coding_agent-0.1.0/tests/test_sbom.py +0 -54
- pulse_coding_agent-0.1.0/tests/test_security_audit_regressions.py +0 -324
- pulse_coding_agent-0.1.0/tests/test_session_manager.py +0 -125
- pulse_coding_agent-0.1.0/tests/test_software_engineer.py +0 -140
- pulse_coding_agent-0.1.0/tests/test_storage.py +0 -111
- pulse_coding_agent-0.1.0/tests/test_streaming.py +0 -199
- pulse_coding_agent-0.1.0/tests/test_subprocesses.py +0 -80
- pulse_coding_agent-0.1.0/tests/test_task_execution_durability.py +0 -194
- pulse_coding_agent-0.1.0/tests/test_task_manager.py +0 -238
- pulse_coding_agent-0.1.0/tests/test_task_manager_concurrency.py +0 -280
- pulse_coding_agent-0.1.0/tests/test_task_manager_crash.py +0 -253
- pulse_coding_agent-0.1.0/tests/test_task_manager_lease_execution.py +0 -507
- pulse_coding_agent-0.1.0/tests/test_task_manager_migration.py +0 -53
- pulse_coding_agent-0.1.0/tests/test_telemetry.py +0 -88
- pulse_coding_agent-0.1.0/tests/test_tool_policy.py +0 -134
- pulse_coding_agent-0.1.0/tests/test_tool_registry.py +0 -52
- pulse_coding_agent-0.1.0/tests/test_verification.py +0 -57
- pulse_coding_agent-0.1.0/uv.lock +0 -1353
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/PRIVACY.md +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/SECURITY.md +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/__main__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/agent.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/agent_manager.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/audit.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/ci/github_client.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/ci/runner.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/cli_ui.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/config.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/context.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/conversations/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/conversations/manager.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/core/agent.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/core/planner.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/core/protocols.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/edits.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/episodic.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/eval/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/eval/trajectory_logger.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/eval/verifier.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/execution/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/execution/remote_task.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/git.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/interactive.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mcp/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mcp/client.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mcp/local_tools.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/memory.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/mutations.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/orchestration/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/orchestration/orchestrator.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/patch.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/planner/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/planner/dag_planner.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/planner/execution_loop.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/production.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/provider.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/provider_keys.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/anthropic.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/base.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/deepseek.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/failover.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/gemini.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/groq.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/manager.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/openai.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/providers/openrouter.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/py.typed +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/reasoning.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/refactor/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/refactor/impact_analyzer.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/repository.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/rpc.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/rule_synthesizer.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/runtime.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/safety/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/safety/safety_manager.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/SECURITY.md +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/api.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/audit.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/base.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/docker.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/host.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/backend/remote.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/errors.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/filesystem.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/git_safe.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/lifecycle.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/network.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/path_validator.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/policy.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/process.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/project.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/python_safe.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/client.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/models.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/protocol.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/server.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/remote/worker.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/resources.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/sandbox/secrets.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/session_manager.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/software_engineer.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/storage.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/streaming.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/subprocesses.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/task_manager.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/telemetry/__init__.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/telemetry/cost_tracker.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/telemetry/logger.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/tool_policy.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/tool_registry.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/tools.py +0 -0
- {pulse_coding_agent-0.1.0 → pulse_coding_agent-0.1.1}/src/pulse/verification.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
55
|
+
python -m pip install pulse-coding-agent==0.1.1
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
157
|
-
secret managers remain supported as
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
25
|
+
python -m pip install pulse-coding-agent==0.1.1
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
126
|
-
secret managers remain supported as
|
|
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.
|
|
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.
|
|
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.
|
|
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]
|
|
@@ -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="
|
|
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
|
|
333
|
-
"""
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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
|
|
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
|
|
360
|
-
"
|
|
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(
|
|
366
|
-
|
|
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(
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
629
|
-
|
|
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(("
|
|
714
|
+
server = SingleRequestHTTPServer(("127.0.0.1", requested_port), OAuthCallbackHandler)
|
|
637
715
|
except OSError as e:
|
|
638
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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(
|
|
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
|
|
541
|
-
print_error(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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.
|