agentsecure 0.1.1__tar.gz → 0.1.3__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 (115) hide show
  1. {agentsecure-0.1.1/agentsecure.egg-info → agentsecure-0.1.3}/PKG-INFO +54 -4
  2. {agentsecure-0.1.1 → agentsecure-0.1.3}/README.md +52 -1
  3. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/__init__.py +1 -2
  4. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/main.py +118 -6
  5. agentsecure-0.1.3/agentsecure/cli/proxy.py +283 -0
  6. agentsecure-0.1.3/agentsecure/cli/receipts.py +207 -0
  7. agentsecure-0.1.3/agentsecure/config/__init__.py +1 -0
  8. agentsecure-0.1.3/agentsecure/config/default_agentsecure.json +49 -0
  9. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/config.py +113 -4
  10. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/container.py +1 -0
  11. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/key_service.py +2 -19
  12. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/models.py +18 -0
  13. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/product.py +7 -35
  14. agentsecure-0.1.3/agentsecure/core/provider_proxy.py +23 -0
  15. agentsecure-0.1.3/agentsecure/gateway/proxy.py +587 -0
  16. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/diff.py +14 -0
  17. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/strategies.py +15 -4
  18. {agentsecure-0.1.1 → agentsecure-0.1.3/agentsecure.egg-info}/PKG-INFO +54 -4
  19. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/SOURCES.txt +9 -1
  20. {agentsecure-0.1.1 → agentsecure-0.1.3}/setup.cfg +7 -4
  21. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_cli.py +30 -0
  22. agentsecure-0.1.3/tests/test_cli_proxy.py +116 -0
  23. agentsecure-0.1.3/tests/test_config_validation.py +81 -0
  24. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_product.py +6 -0
  25. agentsecure-0.1.3/tests/test_provider_proxy.py +277 -0
  26. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_secret_scan.py +11 -0
  27. agentsecure-0.1.3/tests/test_version.py +17 -0
  28. agentsecure-0.1.1/agentsecure/gateway/proxy.py +0 -272
  29. agentsecure-0.1.1/agentsecure.egg-info/requires.txt +0 -3
  30. {agentsecure-0.1.1 → agentsecure-0.1.3}/LICENSE +0 -0
  31. {agentsecure-0.1.1 → agentsecure-0.1.3}/NOTICE +0 -0
  32. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/__main__.py +0 -0
  33. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/api/__init__.py +0 -0
  34. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/api/server.py +0 -0
  35. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/api/services.py +0 -0
  36. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/__init__.py +0 -0
  37. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/common.py +0 -0
  38. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/demo.py +0 -0
  39. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/policy.py +0 -0
  40. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/project.py +0 -0
  41. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/secrets.py +0 -0
  42. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/settings.py +0 -0
  43. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/client/__init__.py +0 -0
  44. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/client/wrappers.py +0 -0
  45. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cloud.py +0 -0
  46. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/__init__.py +0 -0
  47. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/capabilities.py +0 -0
  48. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/command_metadata.py +0 -0
  49. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/config_profiles.py +0 -0
  50. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_mutation.py +0 -0
  51. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_ports.py +0 -0
  52. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_response.py +0 -0
  53. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_validation.py +0 -0
  54. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/time.py +0 -0
  55. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/crypto/__init__.py +0 -0
  56. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/crypto/cipher.py +0 -0
  57. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/crypto/key_provider.py +0 -0
  58. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/__init__.py +0 -0
  59. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/commands.py +0 -0
  60. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/policies.py +0 -0
  61. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/sessions.py +0 -0
  62. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/supervisor.py +0 -0
  63. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/__init__.py +0 -0
  64. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/dotenv_scanner.py +0 -0
  65. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/env_scanner.py +0 -0
  66. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/patterns.py +0 -0
  67. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/scanner.py +0 -0
  68. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/suggestions.py +0 -0
  69. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/gateway/__init__.py +0 -0
  70. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/__init__.py +0 -0
  71. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/command.py +0 -0
  72. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/network.py +0 -0
  73. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/sanitizer.py +0 -0
  74. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/wrappers.py +0 -0
  75. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/__init__.py +0 -0
  76. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/audit.py +0 -0
  77. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/encrypted_secret_store.py +0 -0
  78. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/grant_store.py +0 -0
  79. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/local_secret_store.py +0 -0
  80. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/policy.py +0 -0
  81. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/secret_store_factory.py +0 -0
  82. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/secrets.py +0 -0
  83. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/__init__.py +0 -0
  84. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/audit.py +0 -0
  85. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/grants.py +0 -0
  86. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/key_store.py +0 -0
  87. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/policy.py +0 -0
  88. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/secrets.py +0 -0
  89. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/__init__.py +0 -0
  90. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/apply.py +0 -0
  91. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/materializer.py +0 -0
  92. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/rewriter.py +0 -0
  93. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/dependency_links.txt +0 -0
  94. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/entry_points.txt +0 -0
  95. {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/top_level.txt +0 -0
  96. {agentsecure-0.1.1 → agentsecure-0.1.3}/pyproject.toml +0 -0
  97. {agentsecure-0.1.1 → agentsecure-0.1.3}/setup.py +0 -0
  98. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_audit.py +0 -0
  99. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_cli_demo.py +0 -0
  100. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_cli_policy.py +0 -0
  101. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_diff.py +0 -0
  102. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_discovery.py +0 -0
  103. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_encrypted_secret_store.py +0 -0
  104. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_env_policy.py +0 -0
  105. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_env_policy_contract.py +0 -0
  106. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_gateway_credentials.py +0 -0
  107. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_guard.py +0 -0
  108. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_guard_network.py +0 -0
  109. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_key_service.py +0 -0
  110. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_policy.py +0 -0
  111. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_policy_mutation.py +0 -0
  112. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_secrets.py +0 -0
  113. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_workspace.py +0 -0
  114. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_workspace_apply.py +0 -0
  115. {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentsecure
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Local-first security runtime for AI coding agents
5
5
  Home-page: https://github.com/ShellFrameAI/agentsecure-community
6
6
  Author: ShellFrame AI
@@ -18,11 +18,10 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Topic :: Security
19
19
  Classifier: Environment :: Console
20
20
  Classifier: Operating System :: OS Independent
21
- Requires-Python: >=3.6
21
+ Requires-Python: >=3.8
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  License-File: NOTICE
25
- Requires-Dist: dataclasses; python_version < "3.7"
26
25
  Dynamic: license-file
27
26
 
28
27
  # AgentSecure Community
@@ -33,7 +32,9 @@ Dynamic: license-file
33
32
  [![CI](https://github.com/ShellFrameAI/agentsecure-community/actions/workflows/ci.yml/badge.svg)](https://github.com/ShellFrameAI/agentsecure-community/actions/workflows/ci.yml)
34
33
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
35
34
 
36
- AgentSecure Community is a local-first CLI for AI coding-agent workflows. It demonstrates a simple idea: the agent can work in a real project, but it should see virtual or masked secrets instead of raw `.env` values.
35
+ AI coding agents run where developer secrets already live: `.env` files, shell environments, MCP configs, local credentials, and project settings. GitGuardian's 2026 State of Secrets Sprawl report found [28.65 million new hardcoded secrets in public GitHub commits in 2025](https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/) and [24,008 unique secrets in MCP-related configuration files, including 2,117 valid credentials](https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/). Reported testing has also shown agent tools reading `.env` files despite ignore-file expectations; [The Register reproduced Claude Code reading `.env` with `.claudeignore` and `.gitignore` entries present](https://www.theregister.com/2026/01/28/claude_code_ai_secrets_files/), while [Anthropic's current docs recommend explicit file-access deny rules for sensitive files](https://code.claude.com/docs/en/configuration).
36
+
37
+ AgentSecure Community is a local-first CLI for AI coding-agent workflows. It demonstrates a simple idea: ignore files are not a secret boundary, so the agent should see virtual or masked secrets instead of raw `.env` values.
37
38
 
38
39
  The community release is intentionally scoped to local CLI, local command guard, basic policy config, local secret virtualization, and tests. Hosted cloud sync, enterprise policy management, billing/licensing, and sensitive commercial detection logic are not part of this release.
39
40
 
@@ -106,6 +107,52 @@ By default, `--protect-all` virtualizes discovered secrets. The command output s
106
107
 
107
108
  Denied values are removed only when policy sets `mode: "deny"` for that environment variable. The built-in `agentsecure demo` includes that policy for `DATABASE_URL_PROD` so you can see both behaviors: virtualize and deny.
108
109
 
110
+ ## Provider Proxy Preview
111
+
112
+ Virtual secrets keep real values out of the agent context. Provider proxy mode goes one step further for tools and SDKs that honor `OPENAI_BASE_URL`: the agent gets a virtual key and a local base URL, while AgentSecure injects the real key only when forwarding to the configured provider.
113
+
114
+ Configure OpenAI from `agentsecure.json.provider_catalog.openai`:
115
+
116
+ ```bash
117
+ agentsecure proxy setup openai
118
+ ```
119
+
120
+ Then run the agent:
121
+
122
+ ```bash
123
+ agentsecure run --protect-all -- codex
124
+ ```
125
+
126
+ The agent-visible environment includes:
127
+
128
+ ```text
129
+ OPENAI_API_KEY=virt_openai_...
130
+ OPENAI_BASE_URL=http://127.0.0.1:8765/providers/openai/v1
131
+ ```
132
+
133
+ AgentSecure forwards provider calls to the configured upstream:
134
+
135
+ ```json
136
+ {
137
+ "provider_proxy": {
138
+ "providers": {
139
+ "openai": {
140
+ "upstream": "https://api.openai.com",
141
+ "local_path": "/providers/openai"
142
+ }
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ Run the proxy proof:
149
+
150
+ ```bash
151
+ agentsecure receipts --proxy
152
+ ```
153
+
154
+ Provider proxy mode is local-only. It is not a system-wide proxy, not TLS MITM, and not browser-wide interception. Tools must use the provider base URL environment variable.
155
+
109
156
  ## What It Demonstrates
110
157
 
111
158
  - Discover likely secrets in `.env` files and environment variables.
@@ -155,6 +202,9 @@ agentsecure suggest
155
202
  agentsecure env
156
203
  agentsecure keys list
157
204
  agentsecure network list
205
+ agentsecure proxy setup openai
206
+ agentsecure proxy doctor
207
+ agentsecure receipts --proxy
158
208
  ```
159
209
 
160
210
  Run an agent or command through local command guard:
@@ -6,7 +6,9 @@
6
6
  [![CI](https://github.com/ShellFrameAI/agentsecure-community/actions/workflows/ci.yml/badge.svg)](https://github.com/ShellFrameAI/agentsecure-community/actions/workflows/ci.yml)
7
7
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
8
 
9
- AgentSecure Community is a local-first CLI for AI coding-agent workflows. It demonstrates a simple idea: the agent can work in a real project, but it should see virtual or masked secrets instead of raw `.env` values.
9
+ AI coding agents run where developer secrets already live: `.env` files, shell environments, MCP configs, local credentials, and project settings. GitGuardian's 2026 State of Secrets Sprawl report found [28.65 million new hardcoded secrets in public GitHub commits in 2025](https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/) and [24,008 unique secrets in MCP-related configuration files, including 2,117 valid credentials](https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/). Reported testing has also shown agent tools reading `.env` files despite ignore-file expectations; [The Register reproduced Claude Code reading `.env` with `.claudeignore` and `.gitignore` entries present](https://www.theregister.com/2026/01/28/claude_code_ai_secrets_files/), while [Anthropic's current docs recommend explicit file-access deny rules for sensitive files](https://code.claude.com/docs/en/configuration).
10
+
11
+ AgentSecure Community is a local-first CLI for AI coding-agent workflows. It demonstrates a simple idea: ignore files are not a secret boundary, so the agent should see virtual or masked secrets instead of raw `.env` values.
10
12
 
11
13
  The community release is intentionally scoped to local CLI, local command guard, basic policy config, local secret virtualization, and tests. Hosted cloud sync, enterprise policy management, billing/licensing, and sensitive commercial detection logic are not part of this release.
12
14
 
@@ -79,6 +81,52 @@ By default, `--protect-all` virtualizes discovered secrets. The command output s
79
81
 
80
82
  Denied values are removed only when policy sets `mode: "deny"` for that environment variable. The built-in `agentsecure demo` includes that policy for `DATABASE_URL_PROD` so you can see both behaviors: virtualize and deny.
81
83
 
84
+ ## Provider Proxy Preview
85
+
86
+ Virtual secrets keep real values out of the agent context. Provider proxy mode goes one step further for tools and SDKs that honor `OPENAI_BASE_URL`: the agent gets a virtual key and a local base URL, while AgentSecure injects the real key only when forwarding to the configured provider.
87
+
88
+ Configure OpenAI from `agentsecure.json.provider_catalog.openai`:
89
+
90
+ ```bash
91
+ agentsecure proxy setup openai
92
+ ```
93
+
94
+ Then run the agent:
95
+
96
+ ```bash
97
+ agentsecure run --protect-all -- codex
98
+ ```
99
+
100
+ The agent-visible environment includes:
101
+
102
+ ```text
103
+ OPENAI_API_KEY=virt_openai_...
104
+ OPENAI_BASE_URL=http://127.0.0.1:8765/providers/openai/v1
105
+ ```
106
+
107
+ AgentSecure forwards provider calls to the configured upstream:
108
+
109
+ ```json
110
+ {
111
+ "provider_proxy": {
112
+ "providers": {
113
+ "openai": {
114
+ "upstream": "https://api.openai.com",
115
+ "local_path": "/providers/openai"
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Run the proxy proof:
123
+
124
+ ```bash
125
+ agentsecure receipts --proxy
126
+ ```
127
+
128
+ Provider proxy mode is local-only. It is not a system-wide proxy, not TLS MITM, and not browser-wide interception. Tools must use the provider base URL environment variable.
129
+
82
130
  ## What It Demonstrates
83
131
 
84
132
  - Discover likely secrets in `.env` files and environment variables.
@@ -128,6 +176,9 @@ agentsecure suggest
128
176
  agentsecure env
129
177
  agentsecure keys list
130
178
  agentsecure network list
179
+ agentsecure proxy setup openai
180
+ agentsecure proxy doctor
181
+ agentsecure receipts --proxy
131
182
  ```
132
183
 
133
184
  Run an agent or command through local command guard:
@@ -1,4 +1,3 @@
1
1
  """AgentSecure local-first security runtime."""
2
2
 
3
- __version__ = "0.1.0"
4
-
3
+ __version__ = "0.1.3"
@@ -1,4 +1,5 @@
1
1
  import argparse
2
+ import hashlib
2
3
  import getpass
3
4
  import json
4
5
  import os
@@ -31,6 +32,8 @@ from agentsecure.cli.project import (
31
32
  show_status,
32
33
  uninstall_agentsecure,
33
34
  )
35
+ from agentsecure.cli.proxy import add_proxy_subparser, handle_proxy
36
+ from agentsecure.cli.receipts import handle_receipts
34
37
  from agentsecure.cli.secrets import (
35
38
  discover_secrets,
36
39
  handle_keys,
@@ -56,6 +59,7 @@ from agentsecure.core.capabilities import broker_url_for_env
56
59
  from agentsecure.core.container import Container
57
60
  from agentsecure.core.key_service import KeyManagementError, KeyManagementService
58
61
  from agentsecure.core.models import AgentSecureConfig, DiscoveredSecret, ProcessRequest, SecretReplacement
62
+ from agentsecure.core.provider_proxy import configured_provider_base_url, provider_base_local_path
59
63
  from agentsecure.core.product import ProductService
60
64
  from agentsecure.core.time import DurationError
61
65
  from agentsecure.daemon.commands import CommandExecutor, CommandPoller
@@ -115,6 +119,10 @@ def main(argv: Optional[List[str]] = None) -> int:
115
119
  return handle_files(args)
116
120
  if args.command == "network":
117
121
  return handle_network(args)
122
+ if args.command == "proxy":
123
+ return handle_proxy(args)
124
+ if args.command == "receipts":
125
+ return handle_receipts(args)
118
126
  if args.command == "policy":
119
127
  return handle_policy(args)
120
128
  if args.command == "setup":
@@ -224,6 +232,9 @@ def build_parser() -> argparse.ArgumentParser:
224
232
  network_remove_parser = network_subparsers.add_parser("remove", help="Remove domains from credential allowlist")
225
233
  network_remove_parser.add_argument("domains", nargs="+")
226
234
 
235
+ add_proxy_subparser(subparsers)
236
+ receipts_parser = subparsers.add_parser("receipts", help="Run replayable AgentSecure proof receipts")
237
+ receipts_parser.add_argument("--proxy", action="store_true", help="Run provider proxy receipts")
227
238
  add_policy_subparser(subparsers)
228
239
 
229
240
  setup_parser = subparsers.add_parser("setup", help="Install local protected agent command wrappers")
@@ -265,9 +276,10 @@ def run_agent(args: argparse.Namespace) -> int:
265
276
  if isinstance(replacements, int):
266
277
  return replacements
267
278
  container = Container.from_config_path(args.config)
279
+ replacements.extend(_configured_secret_replacements(container, replacements))
268
280
  run_cwd = os.getcwd()
269
281
  workspace_session = None
270
- materializer = WorkspaceMaterializer()
282
+ materializer = WorkspaceMaterializer(_workspace_base_for_runtime(run_cwd, args.runtime))
271
283
  should_create_workspace = bool(replacements or container.config.files.protect_write)
272
284
  if args.runtime == "command-guard":
273
285
  print("AgentSecure runtime: command-guard", flush=True)
@@ -345,6 +357,7 @@ def run_agent(args: argparse.Namespace) -> int:
345
357
  env = os.environ.copy()
346
358
  for env_name in container.config.env_policy.rules:
347
359
  env.pop(env_name, None)
360
+ _strip_backing_secret_environment(env, container)
348
361
  env.update(container.virtual_env_provider.build_environment())
349
362
  if args.runtime == "command-guard":
350
363
  CommandGuardWrapperInstaller(args.config).install(env)
@@ -353,6 +366,7 @@ def run_agent(args: argparse.Namespace) -> int:
353
366
  env["AGENTSECURE_SESSION_ID"] = session_id
354
367
  proxy_url = _proxy_url(gateway_host, gateway_port, session_id)
355
368
  _apply_proxy_environment(env, proxy_url)
369
+ _apply_provider_proxy_environment(env, container, gateway_host, gateway_port)
356
370
  command_metadata = safe_command_metadata(argv)
357
371
  container.audit_logger.record(
358
372
  "agent_started",
@@ -389,7 +403,8 @@ def run_agent(args: argparse.Namespace) -> int:
389
403
  os.environ["AGENTSECURE_CLOUD_DEBUG"] = "true"
390
404
  session_finished = False
391
405
  try:
392
- process = _start_agent_process(argv, env, run_cwd)
406
+ output_sanitizer = SecretOutputSanitizer.from_config_path(args.config)
407
+ process = _start_agent_process(argv, env, run_cwd, sanitize_output=True)
393
408
  process_group_id = _process_group_id(process)
394
409
  if cloud_session and cloud and cloud.status().get("enrolled"):
395
410
  command_executor = _run_command_executor(
@@ -414,7 +429,7 @@ def run_agent(args: argparse.Namespace) -> int:
414
429
  elif command_executor:
415
430
  command_poller = CommandPoller(cloud, command_executor)
416
431
  command_poller.start()
417
- exit_code = process.wait()
432
+ exit_code = _wait_for_agent_process(process, output_sanitizer)
418
433
  if exit_code is None or exit_code >= 0:
419
434
  _wait_for_process_group_exit(process_group_id)
420
435
  final_status = "killed" if exit_code is not None and exit_code < 0 else "finished"
@@ -452,10 +467,51 @@ def run_agent(args: argparse.Namespace) -> int:
452
467
  )
453
468
 
454
469
 
455
- def _start_agent_process(argv: List[str], env, cwd: str):
470
+ def _workspace_base_for_runtime(source_root: str, runtime: str) -> str:
471
+ if runtime != "workspace":
472
+ return ".agentsecure/workspaces"
473
+ digest = hashlib.sha256(os.path.abspath(source_root).encode("utf-8")).hexdigest()[:16]
474
+ return os.path.join(tempfile.gettempdir(), "agentsecure-workspaces", digest)
475
+
476
+
477
+ def _start_agent_process(argv: List[str], env, cwd: str, sanitize_output: bool = False):
478
+ stdout = subprocess.PIPE if sanitize_output else None
479
+ stderr = subprocess.PIPE if sanitize_output else None
456
480
  if os.name == "posix":
457
- return subprocess.Popen(argv, env=env, cwd=cwd, preexec_fn=os.setsid)
458
- return subprocess.Popen(argv, env=env, cwd=cwd)
481
+ return subprocess.Popen(argv, env=env, cwd=cwd, stdout=stdout, stderr=stderr, preexec_fn=os.setsid)
482
+ return subprocess.Popen(argv, env=env, cwd=cwd, stdout=stdout, stderr=stderr)
483
+
484
+
485
+ def _wait_for_agent_process(process, sanitizer: SecretOutputSanitizer) -> int:
486
+ if process.stdout is None and process.stderr is None:
487
+ return process.wait()
488
+
489
+ stdout_thread = _start_output_forwarder(process.stdout, sys.stdout.buffer, sanitizer)
490
+ stderr_thread = _start_output_forwarder(process.stderr, sys.stderr.buffer, sanitizer)
491
+ exit_code = process.wait()
492
+ if stdout_thread:
493
+ stdout_thread.join()
494
+ if stderr_thread:
495
+ stderr_thread.join()
496
+ return int(exit_code)
497
+
498
+
499
+ def _start_output_forwarder(source, target, sanitizer: SecretOutputSanitizer):
500
+ if source is None:
501
+ return None
502
+
503
+ def forward() -> None:
504
+ while True:
505
+ chunk = source.readline()
506
+ if not chunk:
507
+ break
508
+ target.write(sanitizer.sanitize_bytes(chunk))
509
+ target.flush()
510
+
511
+ thread = threading.Thread(target=forward)
512
+ thread.daemon = True
513
+ thread.start()
514
+ return thread
459
515
 
460
516
 
461
517
  def _process_group_id(process) -> int:
@@ -582,6 +638,7 @@ def _start_local_gateway_thread(container: Container, host: str, port: int):
582
638
  container.token_resolver,
583
639
  container.audit_logger,
584
640
  container.bindings,
641
+ container.config.provider_proxy,
585
642
  )
586
643
 
587
644
  def run_gateway_thread() -> None:
@@ -766,6 +823,59 @@ def _apply_proxy_environment(env, proxy_url: str) -> None:
766
823
  env["no_proxy"] = no_proxy
767
824
 
768
825
 
826
+ def _apply_provider_proxy_environment(env, container: Container, gateway_host: str, gateway_port: int) -> None:
827
+ if not container.config.provider_proxy.enabled:
828
+ return
829
+ raw = dict(container.config.raw)
830
+ gateway = dict(raw.get("gateway", {}))
831
+ gateway["host"] = gateway_host
832
+ gateway["port"] = gateway_port
833
+ raw["gateway"] = gateway
834
+ for provider in container.config.provider_proxy.providers.values():
835
+ provider_data = {
836
+ "local_path": provider_base_local_path(provider.local_path, provider.allow_paths),
837
+ }
838
+ env[provider.base_url_env] = configured_provider_base_url(raw, provider_data)
839
+
840
+
841
+ def _strip_backing_secret_environment(env, container: Container) -> None:
842
+ for binding in container.config.secrets:
843
+ if binding.real_secret_env:
844
+ env.pop(binding.real_secret_env, None)
845
+
846
+
847
+ def _configured_secret_replacements(container: Container, existing: List[SecretReplacement]) -> List[SecretReplacement]:
848
+ existing_names = set(replacement.name for replacement in existing)
849
+ replacements = []
850
+ for binding in container.config.secrets:
851
+ if binding.env_name in existing_names:
852
+ continue
853
+ real_value = container.token_resolver.resolve(binding.virtual_token) or ""
854
+ if not real_value:
855
+ continue
856
+ rule = container.config.env_policy.rule_for(binding.env_name)
857
+ if rule.mode == "deny":
858
+ replacements.append(
859
+ SecretReplacement(
860
+ source="configured",
861
+ name=binding.env_name,
862
+ real_value=real_value,
863
+ virtual_value="",
864
+ action="remove",
865
+ )
866
+ )
867
+ elif rule.mode != "broker":
868
+ replacements.append(
869
+ SecretReplacement(
870
+ source="configured",
871
+ name=binding.env_name,
872
+ real_value=real_value,
873
+ virtual_value=binding.virtual_token,
874
+ )
875
+ )
876
+ return replacements
877
+
878
+
769
879
  def _merge_no_proxy(existing: str) -> str:
770
880
  defaults = [
771
881
  "localhost",
@@ -782,6 +892,8 @@ def _merge_no_proxy(existing: str) -> str:
782
892
  if not value:
783
893
  continue
784
894
  key = value.lower()
895
+ if key == "*":
896
+ continue
785
897
  if key not in seen:
786
898
  values.append(value)
787
899
  seen.add(key)
@@ -0,0 +1,283 @@
1
+ import argparse
2
+ import os
3
+ import re
4
+ import socket
5
+ import sys
6
+ from typing import Any, Dict
7
+ from urllib.parse import urlsplit
8
+
9
+ from agentsecure.cli.common import load_config_data, normalize_domain
10
+ from agentsecure.core.config import ConfigError, JsonConfigLoader, JsonConfigWriter
11
+ from agentsecure.core.models import Destination
12
+ from agentsecure.core.provider_proxy import configured_provider_base_url, provider_base_local_path, upstream_host
13
+ from agentsecure.core.product import default_config
14
+ from agentsecure.implementations.policy import StrictDestinationValidator
15
+
16
+
17
+ def add_proxy_subparser(subparsers) -> None:
18
+ proxy_parser = subparsers.add_parser("proxy", help="Configure local provider proxy")
19
+ proxy_subparsers = proxy_parser.add_subparsers(dest="proxy_command")
20
+
21
+ setup_parser = proxy_subparsers.add_parser("setup", help="Configure a provider proxy")
22
+ setup_subparsers = setup_parser.add_subparsers(dest="provider")
23
+ openai_parser = setup_subparsers.add_parser("openai", help="Activate provider_catalog.openai")
24
+ openai_parser.add_argument(
25
+ "--trust-local-catalog",
26
+ action="store_true",
27
+ help="Allow provider_catalog.openai values that differ from the packaged default",
28
+ )
29
+
30
+ custom_parser = setup_subparsers.add_parser("custom", help="Configure a custom provider proxy")
31
+ custom_parser.add_argument("--name", required=True, help="Provider name used in the local path")
32
+ custom_parser.add_argument("--upstream", required=True, help="Provider upstream, for example https://api.example.com")
33
+ custom_parser.add_argument("--env", required=True, help="Environment variable that contains the provider key")
34
+ custom_parser.add_argument("--base-url-env", required=True, help="Environment variable used by the SDK for base URL")
35
+ custom_parser.add_argument("--allow-path", action="append", default=[], help="Allowed upstream path prefix")
36
+
37
+ proxy_subparsers.add_parser("doctor", help="Check provider proxy setup")
38
+
39
+
40
+ def handle_proxy(args: argparse.Namespace) -> int:
41
+ if args.proxy_command == "setup":
42
+ return setup_proxy(args)
43
+ if args.proxy_command == "doctor":
44
+ return doctor_proxy(args)
45
+ sys.stderr.write("agentsecure: missing proxy subcommand\n")
46
+ return 2
47
+
48
+
49
+ def setup_proxy(args: argparse.Namespace) -> int:
50
+ if args.provider == "openai":
51
+ return _activate_catalog_provider(args.config, "openai", args.trust_local_catalog)
52
+ if args.provider == "custom":
53
+ return _activate_custom_provider(args)
54
+ sys.stderr.write("agentsecure: missing proxy provider\n")
55
+ return 2
56
+
57
+
58
+ def doctor_proxy(args: argparse.Namespace) -> int:
59
+ try:
60
+ config = JsonConfigLoader().load(args.config)
61
+ except (ConfigError, OSError, ValueError) as exc:
62
+ sys.stderr.write("agentsecure: invalid config: %s\n" % exc)
63
+ return 1
64
+
65
+ if not config.provider_proxy.enabled:
66
+ print("Provider proxy: disabled")
67
+ return 1
68
+
69
+ ok = True
70
+ if _can_bind(config.gateway.host, config.gateway.port):
71
+ print("Gateway bind: ok (%s:%s)" % (config.gateway.host, config.gateway.port))
72
+ else:
73
+ print("Gateway bind: blocked or busy (%s:%s)" % (config.gateway.host, config.gateway.port))
74
+ ok = False
75
+
76
+ bindings = {(binding.env_name, binding.provider): binding for binding in config.secrets}
77
+ network_validator = StrictDestinationValidator(config.network)
78
+ for provider in config.provider_proxy.providers.values():
79
+ host = normalize_domain(urlsplit(provider.upstream).hostname or "")
80
+ print("Provider %s:" % provider.name)
81
+ local_path = provider_base_local_path(provider.local_path, provider.allow_paths)
82
+ print(" local: http://%s:%s%s" % (config.gateway.host, config.gateway.port, local_path))
83
+ print(" upstream: %s" % provider.upstream)
84
+ print(" env: %s" % provider.env_name)
85
+ print(" base url env: %s" % provider.base_url_env)
86
+ if (provider.env_name, provider.name) in bindings:
87
+ print(" binding: ok")
88
+ else:
89
+ print(" binding: missing (run with --protect-all after adding the key for provider %s)" % provider.name)
90
+ ok = False
91
+ parsed = urlsplit(provider.upstream)
92
+ port = parsed.port or 443
93
+ decision = network_validator.validate(Destination("https", host, port, credentials_present=True))
94
+ if decision.allowed:
95
+ print(" network allowlist: ok")
96
+ else:
97
+ print(" network policy: blocked (%s)" % decision.reason)
98
+ ok = False
99
+ return 0 if ok else 1
100
+
101
+
102
+ def _activate_catalog_provider(config_path: str, provider_name: str, trust_local_catalog: bool = False) -> int:
103
+ config = load_config_data(config_path)
104
+ catalog = config.get("provider_catalog", {})
105
+ provider = catalog.get(provider_name)
106
+ if not isinstance(provider, dict):
107
+ sys.stderr.write(
108
+ "agentsecure: provider_catalog.%s is missing from %s\n" % (provider_name, config_path)
109
+ )
110
+ return 1
111
+ default_provider = default_config().get("provider_catalog", {}).get(provider_name)
112
+ if default_provider and not trust_local_catalog and _canonical_provider(provider) != _canonical_provider(default_provider):
113
+ sys.stderr.write(
114
+ "agentsecure: provider_catalog.%s differs from the packaged default; "
115
+ "use proxy setup custom or pass --trust-local-catalog\n" % provider_name
116
+ )
117
+ return 2
118
+ return _activate_provider(config_path, config, provider_name, provider)
119
+
120
+
121
+ def _activate_custom_provider(args: argparse.Namespace) -> int:
122
+ config = load_config_data(args.config)
123
+ name = _slug(args.name)
124
+ upstream = str(args.upstream).rstrip("/")
125
+ parsed = urlsplit(upstream)
126
+ try:
127
+ parsed_port = parsed.port
128
+ except ValueError:
129
+ sys.stderr.write("agentsecure: custom upstream port is invalid\n")
130
+ return 2
131
+ if parsed.scheme != "https" or not parsed.hostname:
132
+ sys.stderr.write("agentsecure: custom upstream must be https and include a host\n")
133
+ return 2
134
+ _ = parsed_port
135
+ provider = {
136
+ "env_name": args.env,
137
+ "base_url_env": args.base_url_env,
138
+ "upstream": upstream,
139
+ "local_path": "/providers/%s" % name,
140
+ "inject_as": "authorization_bearer",
141
+ "allow_paths": args.allow_path or ["/"],
142
+ "allow_domains": [parsed.hostname],
143
+ }
144
+ config.setdefault("provider_catalog", {})[name] = dict(provider)
145
+ return _activate_provider(args.config, config, name, provider)
146
+
147
+
148
+ def _activate_provider(
149
+ config_path: str,
150
+ config: Dict[str, Any],
151
+ provider_name: str,
152
+ provider: Dict[str, Any],
153
+ ) -> int:
154
+ error = _validate_provider(provider_name, provider)
155
+ if error:
156
+ sys.stderr.write("agentsecure: %s\n" % error)
157
+ return 2
158
+
159
+ provider_proxy = config.setdefault("provider_proxy", {})
160
+ provider_proxy["enabled"] = True
161
+ providers = provider_proxy.setdefault("providers", {})
162
+ providers[provider_name] = {
163
+ "env_name": str(provider["env_name"]),
164
+ "base_url_env": str(provider["base_url_env"]),
165
+ "upstream": str(provider["upstream"]).rstrip("/"),
166
+ "local_path": _normalize_path(str(provider["local_path"])),
167
+ "inject_as": "authorization_bearer",
168
+ "allow_paths": [_normalize_allow_path(path) for path in provider.get("allow_paths", ["/"])],
169
+ }
170
+
171
+ env_name = str(provider["env_name"])
172
+ env_policy = config.setdefault("env_policy", {})
173
+ rule = dict(env_policy.get(env_name, {}))
174
+ rule.setdefault("mode", "virtualize")
175
+ rule.setdefault(
176
+ "reason",
177
+ "Agent sees a virtual token; AgentSecure injects the real key only at the configured provider proxy.",
178
+ )
179
+ rule["approved_hosts"] = list(provider.get("allow_domains") or [upstream_host(provider)])
180
+ env_policy[env_name] = rule
181
+
182
+ network = config.setdefault("network", {})
183
+ allowed = list(network.get("allow_domains", []))
184
+ for domain in provider.get("allow_domains") or [upstream_host(provider)]:
185
+ normalized = normalize_domain(str(domain))
186
+ if normalized and normalized not in allowed:
187
+ allowed.append(normalized)
188
+ network["allow_domains"] = allowed
189
+
190
+ JsonConfigWriter().save(config_path, config)
191
+ active_provider = dict(providers[provider_name])
192
+ active_provider["local_path"] = provider_base_local_path(
193
+ str(active_provider["local_path"]),
194
+ list(active_provider.get("allow_paths") or []),
195
+ )
196
+ print("Configured provider proxy: %s" % provider_name)
197
+ print(" %s=%s" % (provider["base_url_env"], configured_provider_base_url(config, active_provider)))
198
+ print(" %s stays virtualized" % provider["env_name"])
199
+ print(" upstream: %s" % provider["upstream"])
200
+ return 0
201
+
202
+
203
+ def _validate_provider(provider_name: str, provider: Dict[str, Any]) -> str:
204
+ for key in ("env_name", "base_url_env", "upstream", "local_path"):
205
+ if not str(provider.get(key, "")).strip():
206
+ return "provider_catalog.%s.%s is required" % (provider_name, key)
207
+ upstream = str(provider.get("upstream", "")).strip()
208
+ parsed = urlsplit(upstream)
209
+ try:
210
+ parsed_port = parsed.port
211
+ except ValueError:
212
+ return "provider_catalog.%s.upstream port is invalid" % provider_name
213
+ if parsed.scheme != "https" or not parsed.hostname:
214
+ return "provider_catalog.%s.upstream must be an https URL with a host" % provider_name
215
+ _ = parsed_port
216
+ if _normalize_path(str(provider.get("local_path", ""))) == "/":
217
+ return "provider_catalog.%s.local_path must not be /" % provider_name
218
+ if not isinstance(provider.get("allow_paths", []), list):
219
+ return "provider_catalog.%s.allow_paths must be a list" % provider_name
220
+ for key in ("env_name", "base_url_env"):
221
+ error = _validate_env_name(str(provider[key]), "provider_catalog.%s.%s" % (provider_name, key))
222
+ if error:
223
+ return error
224
+ if str(provider.get("inject_as", "authorization_bearer")) != "authorization_bearer":
225
+ return "provider_catalog.%s.inject_as must be authorization_bearer" % provider_name
226
+ return ""
227
+
228
+
229
+ def _validate_env_name(value: str, path: str) -> str:
230
+ if not re.match(r"^[A-Z_][A-Z0-9_]*$", value):
231
+ return "%s must be an uppercase environment variable name" % path
232
+ blocked = {
233
+ "PATH",
234
+ "HOME",
235
+ "SHELL",
236
+ "PYTHONPATH",
237
+ "HTTP_PROXY",
238
+ "HTTPS_PROXY",
239
+ "NO_PROXY",
240
+ }
241
+ if value in blocked:
242
+ return "%s must not override critical process environment" % path
243
+ return ""
244
+
245
+
246
+ def _canonical_provider(provider: Dict[str, Any]) -> Dict[str, Any]:
247
+ return {
248
+ "env_name": str(provider.get("env_name", "")),
249
+ "base_url_env": str(provider.get("base_url_env", "")),
250
+ "upstream": str(provider.get("upstream", "")).rstrip("/"),
251
+ "local_path": _normalize_path(str(provider.get("local_path", ""))),
252
+ "inject_as": str(provider.get("inject_as", "authorization_bearer")),
253
+ "allow_paths": [_normalize_allow_path(path) for path in provider.get("allow_paths", [])],
254
+ "allow_domains": [normalize_domain(str(domain)) for domain in provider.get("allow_domains", [])],
255
+ }
256
+
257
+
258
+ def _normalize_path(path: str) -> str:
259
+ return "/" + path.strip().strip("/")
260
+
261
+
262
+ def _normalize_allow_path(path: str) -> str:
263
+ normalized = "/" + str(path).strip().strip("/")
264
+ return "/" if normalized == "/" else normalized.rstrip("/") + "/"
265
+
266
+
267
+ def _slug(value: str) -> str:
268
+ slug = "".join(ch.lower() if ch.isalnum() else "-" for ch in value).strip("-")
269
+ while "--" in slug:
270
+ slug = slug.replace("--", "-")
271
+ return slug or "custom"
272
+
273
+
274
+ def _can_bind(host: str, port: int) -> bool:
275
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
276
+ try:
277
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
278
+ sock.bind((host, port))
279
+ return True
280
+ except OSError:
281
+ return False
282
+ finally:
283
+ sock.close()