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.
- {agentsecure-0.1.2/agentsecure.egg-info → agentsecure-0.1.3}/PKG-INFO +51 -3
- {agentsecure-0.1.2 → agentsecure-0.1.3}/README.md +49 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/__init__.py +1 -2
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/main.py +69 -0
- agentsecure-0.1.3/agentsecure/cli/proxy.py +283 -0
- agentsecure-0.1.3/agentsecure/cli/receipts.py +207 -0
- agentsecure-0.1.3/agentsecure/config/__init__.py +1 -0
- agentsecure-0.1.3/agentsecure/config/default_agentsecure.json +49 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/config.py +113 -4
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/container.py +1 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/key_service.py +2 -19
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/models.py +18 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/product.py +7 -35
- agentsecure-0.1.3/agentsecure/core/provider_proxy.py +23 -0
- agentsecure-0.1.3/agentsecure/gateway/proxy.py +587 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3/agentsecure.egg-info}/PKG-INFO +51 -3
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/SOURCES.txt +9 -1
- {agentsecure-0.1.2 → agentsecure-0.1.3}/setup.cfg +7 -4
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_cli.py +30 -0
- agentsecure-0.1.3/tests/test_cli_proxy.py +116 -0
- agentsecure-0.1.3/tests/test_config_validation.py +81 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_product.py +6 -0
- agentsecure-0.1.3/tests/test_provider_proxy.py +277 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_secret_scan.py +11 -0
- agentsecure-0.1.3/tests/test_version.py +17 -0
- agentsecure-0.1.2/agentsecure/gateway/proxy.py +0 -272
- agentsecure-0.1.2/agentsecure.egg-info/requires.txt +0 -3
- {agentsecure-0.1.2 → agentsecure-0.1.3}/LICENSE +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/NOTICE +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/__main__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/api/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/api/server.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/api/services.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/common.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/demo.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/policy.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/project.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/secrets.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cli/settings.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/client/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/client/wrappers.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/cloud.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/capabilities.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/command_metadata.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/config_profiles.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_mutation.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_ports.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_response.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/policy_validation.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/core/time.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/crypto/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/crypto/cipher.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/crypto/key_provider.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/commands.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/policies.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/sessions.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/daemon/supervisor.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/dotenv_scanner.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/env_scanner.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/patterns.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/scanner.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/discovery/suggestions.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/gateway/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/command.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/network.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/sanitizer.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/guard/wrappers.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/audit.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/encrypted_secret_store.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/grant_store.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/local_secret_store.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/policy.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/secret_store_factory.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/implementations/secrets.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/audit.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/grants.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/key_store.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/policy.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/interfaces/secrets.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/__init__.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/apply.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/diff.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/materializer.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/rewriter.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure/workspace/strategies.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/dependency_links.txt +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/entry_points.txt +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/agentsecure.egg-info/top_level.txt +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/pyproject.toml +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/setup.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_audit.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_cli_demo.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_cli_policy.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_diff.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_discovery.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_encrypted_secret_store.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_env_policy.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_env_policy_contract.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_gateway_credentials.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_guard.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_guard_network.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_key_service.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_policy.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_policy_mutation.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_secrets.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_workspace.py +0 -0
- {agentsecure-0.1.2 → agentsecure-0.1.3}/tests/test_workspace_apply.py +0 -0
- {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.
|
|
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.
|
|
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:
|
|
@@ -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()
|