agentsecure 0.1.2__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.2/agentsecure.egg-info → agentsecure-0.1.3}/PKG-INFO +51 -3
  2. {agentsecure-0.1.2 → agentsecure-0.1.3}/README.md +49 -0
  3. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/__init__.py +1 -2
  4. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/main.py +69 -0
  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.2 → agentsecure-0.1.3}/agentsecure/core/config.py +113 -4
  10. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/container.py +1 -0
  11. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/key_service.py +2 -19
  12. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/models.py +18 -0
  13. {agentsecure-0.1.2 → 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.2 → agentsecure-0.1.3/agentsecure.egg-info}/PKG-INFO +51 -3
  17. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/SOURCES.txt +9 -1
  18. {agentsecure-0.1.2 → agentsecure-0.1.3}/setup.cfg +7 -4
  19. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_cli.py +30 -0
  20. agentsecure-0.1.3/tests/test_cli_proxy.py +116 -0
  21. agentsecure-0.1.3/tests/test_config_validation.py +81 -0
  22. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_product.py +6 -0
  23. agentsecure-0.1.3/tests/test_provider_proxy.py +277 -0
  24. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_secret_scan.py +11 -0
  25. agentsecure-0.1.3/tests/test_version.py +17 -0
  26. agentsecure-0.1.2/agentsecure/gateway/proxy.py +0 -272
  27. agentsecure-0.1.2/agentsecure.egg-info/requires.txt +0 -3
  28. {agentsecure-0.1.2 → agentsecure-0.1.3}/LICENSE +0 -0
  29. {agentsecure-0.1.2 → agentsecure-0.1.3}/NOTICE +0 -0
  30. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/__main__.py +0 -0
  31. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/api/__init__.py +0 -0
  32. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/api/server.py +0 -0
  33. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/api/services.py +0 -0
  34. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/__init__.py +0 -0
  35. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/common.py +0 -0
  36. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/demo.py +0 -0
  37. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/policy.py +0 -0
  38. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/project.py +0 -0
  39. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/secrets.py +0 -0
  40. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/settings.py +0 -0
  41. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/client/__init__.py +0 -0
  42. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/client/wrappers.py +0 -0
  43. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cloud.py +0 -0
  44. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/__init__.py +0 -0
  45. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/capabilities.py +0 -0
  46. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/command_metadata.py +0 -0
  47. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/config_profiles.py +0 -0
  48. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_mutation.py +0 -0
  49. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_ports.py +0 -0
  50. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_response.py +0 -0
  51. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_validation.py +0 -0
  52. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/time.py +0 -0
  53. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/crypto/__init__.py +0 -0
  54. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/crypto/cipher.py +0 -0
  55. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/crypto/key_provider.py +0 -0
  56. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/__init__.py +0 -0
  57. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/commands.py +0 -0
  58. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/policies.py +0 -0
  59. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/sessions.py +0 -0
  60. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/supervisor.py +0 -0
  61. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/__init__.py +0 -0
  62. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/dotenv_scanner.py +0 -0
  63. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/env_scanner.py +0 -0
  64. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/patterns.py +0 -0
  65. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/scanner.py +0 -0
  66. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/suggestions.py +0 -0
  67. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/gateway/__init__.py +0 -0
  68. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/__init__.py +0 -0
  69. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/command.py +0 -0
  70. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/network.py +0 -0
  71. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/sanitizer.py +0 -0
  72. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/wrappers.py +0 -0
  73. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/__init__.py +0 -0
  74. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/audit.py +0 -0
  75. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/encrypted_secret_store.py +0 -0
  76. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/grant_store.py +0 -0
  77. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/local_secret_store.py +0 -0
  78. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/policy.py +0 -0
  79. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/secret_store_factory.py +0 -0
  80. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/secrets.py +0 -0
  81. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/__init__.py +0 -0
  82. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/audit.py +0 -0
  83. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/grants.py +0 -0
  84. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/key_store.py +0 -0
  85. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/policy.py +0 -0
  86. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/secrets.py +0 -0
  87. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/__init__.py +0 -0
  88. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/apply.py +0 -0
  89. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/diff.py +0 -0
  90. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/materializer.py +0 -0
  91. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/rewriter.py +0 -0
  92. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/strategies.py +0 -0
  93. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/dependency_links.txt +0 -0
  94. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/entry_points.txt +0 -0
  95. {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/top_level.txt +0 -0
  96. {agentsecure-0.1.2 → agentsecure-0.1.3}/pyproject.toml +0 -0
  97. {agentsecure-0.1.2 → agentsecure-0.1.3}/setup.py +0 -0
  98. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_audit.py +0 -0
  99. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_cli_demo.py +0 -0
  100. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_cli_policy.py +0 -0
  101. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_diff.py +0 -0
  102. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_discovery.py +0 -0
  103. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_encrypted_secret_store.py +0 -0
  104. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_env_policy.py +0 -0
  105. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_env_policy_contract.py +0 -0
  106. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_gateway_credentials.py +0 -0
  107. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_guard.py +0 -0
  108. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_guard_network.py +0 -0
  109. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_key_service.py +0 -0
  110. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_policy.py +0 -0
  111. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_policy_mutation.py +0 -0
  112. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_secrets.py +0 -0
  113. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_workspace.py +0 -0
  114. {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_workspace_apply.py +0 -0
  115. {agentsecure-0.1.2 → 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.2
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
@@ -108,6 +107,52 @@ By default, `--protect-all` virtualizes discovered secrets. The command output s
108
107
 
109
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.
110
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
+
111
156
  ## What It Demonstrates
112
157
 
113
158
  - Discover likely secrets in `.env` files and environment variables.
@@ -157,6 +202,9 @@ agentsecure suggest
157
202
  agentsecure env
158
203
  agentsecure keys list
159
204
  agentsecure network list
205
+ agentsecure proxy setup openai
206
+ agentsecure proxy doctor
207
+ agentsecure receipts --proxy
160
208
  ```
161
209
 
162
210
  Run an agent or command through local command guard:
@@ -81,6 +81,52 @@ By default, `--protect-all` virtualizes discovered secrets. The command output s
81
81
 
82
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.
83
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
+
84
130
  ## What It Demonstrates
85
131
 
86
132
  - Discover likely secrets in `.env` files and environment variables.
@@ -130,6 +176,9 @@ agentsecure suggest
130
176
  agentsecure env
131
177
  agentsecure keys list
132
178
  agentsecure network list
179
+ agentsecure proxy setup openai
180
+ agentsecure proxy doctor
181
+ agentsecure receipts --proxy
133
182
  ```
134
183
 
135
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"
@@ -32,6 +32,8 @@ from agentsecure.cli.project import (
32
32
  show_status,
33
33
  uninstall_agentsecure,
34
34
  )
35
+ from agentsecure.cli.proxy import add_proxy_subparser, handle_proxy
36
+ from agentsecure.cli.receipts import handle_receipts
35
37
  from agentsecure.cli.secrets import (
36
38
  discover_secrets,
37
39
  handle_keys,
@@ -57,6 +59,7 @@ from agentsecure.core.capabilities import broker_url_for_env
57
59
  from agentsecure.core.container import Container
58
60
  from agentsecure.core.key_service import KeyManagementError, KeyManagementService
59
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
60
63
  from agentsecure.core.product import ProductService
61
64
  from agentsecure.core.time import DurationError
62
65
  from agentsecure.daemon.commands import CommandExecutor, CommandPoller
@@ -116,6 +119,10 @@ def main(argv: Optional[List[str]] = None) -> int:
116
119
  return handle_files(args)
117
120
  if args.command == "network":
118
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)
119
126
  if args.command == "policy":
120
127
  return handle_policy(args)
121
128
  if args.command == "setup":
@@ -225,6 +232,9 @@ def build_parser() -> argparse.ArgumentParser:
225
232
  network_remove_parser = network_subparsers.add_parser("remove", help="Remove domains from credential allowlist")
226
233
  network_remove_parser.add_argument("domains", nargs="+")
227
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")
228
238
  add_policy_subparser(subparsers)
229
239
 
230
240
  setup_parser = subparsers.add_parser("setup", help="Install local protected agent command wrappers")
@@ -266,6 +276,7 @@ def run_agent(args: argparse.Namespace) -> int:
266
276
  if isinstance(replacements, int):
267
277
  return replacements
268
278
  container = Container.from_config_path(args.config)
279
+ replacements.extend(_configured_secret_replacements(container, replacements))
269
280
  run_cwd = os.getcwd()
270
281
  workspace_session = None
271
282
  materializer = WorkspaceMaterializer(_workspace_base_for_runtime(run_cwd, args.runtime))
@@ -346,6 +357,7 @@ def run_agent(args: argparse.Namespace) -> int:
346
357
  env = os.environ.copy()
347
358
  for env_name in container.config.env_policy.rules:
348
359
  env.pop(env_name, None)
360
+ _strip_backing_secret_environment(env, container)
349
361
  env.update(container.virtual_env_provider.build_environment())
350
362
  if args.runtime == "command-guard":
351
363
  CommandGuardWrapperInstaller(args.config).install(env)
@@ -354,6 +366,7 @@ def run_agent(args: argparse.Namespace) -> int:
354
366
  env["AGENTSECURE_SESSION_ID"] = session_id
355
367
  proxy_url = _proxy_url(gateway_host, gateway_port, session_id)
356
368
  _apply_proxy_environment(env, proxy_url)
369
+ _apply_provider_proxy_environment(env, container, gateway_host, gateway_port)
357
370
  command_metadata = safe_command_metadata(argv)
358
371
  container.audit_logger.record(
359
372
  "agent_started",
@@ -625,6 +638,7 @@ def _start_local_gateway_thread(container: Container, host: str, port: int):
625
638
  container.token_resolver,
626
639
  container.audit_logger,
627
640
  container.bindings,
641
+ container.config.provider_proxy,
628
642
  )
629
643
 
630
644
  def run_gateway_thread() -> None:
@@ -809,6 +823,59 @@ def _apply_proxy_environment(env, proxy_url: str) -> None:
809
823
  env["no_proxy"] = no_proxy
810
824
 
811
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
+
812
879
  def _merge_no_proxy(existing: str) -> str:
813
880
  defaults = [
814
881
  "localhost",
@@ -825,6 +892,8 @@ def _merge_no_proxy(existing: str) -> str:
825
892
  if not value:
826
893
  continue
827
894
  key = value.lower()
895
+ if key == "*":
896
+ continue
828
897
  if key not in seen:
829
898
  values.append(value)
830
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()