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.
- {agentsecure-0.1.1/agentsecure.egg-info → agentsecure-0.1.3}/PKG-INFO +54 -4
- {agentsecure-0.1.1 → agentsecure-0.1.3}/README.md +52 -1
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/__init__.py +1 -2
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/main.py +118 -6
- 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.1 → agentsecure-0.1.3}/agentsecure/core/config.py +113 -4
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/container.py +1 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/key_service.py +2 -19
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/models.py +18 -0
- {agentsecure-0.1.1 → 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.1 → agentsecure-0.1.3}/agentsecure/workspace/diff.py +14 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/strategies.py +15 -4
- {agentsecure-0.1.1 → agentsecure-0.1.3/agentsecure.egg-info}/PKG-INFO +54 -4
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/SOURCES.txt +9 -1
- {agentsecure-0.1.1 → agentsecure-0.1.3}/setup.cfg +7 -4
- {agentsecure-0.1.1 → 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.1 → agentsecure-0.1.3}/tests/test_product.py +6 -0
- agentsecure-0.1.3/tests/test_provider_proxy.py +277 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_secret_scan.py +11 -0
- agentsecure-0.1.3/tests/test_version.py +17 -0
- agentsecure-0.1.1/agentsecure/gateway/proxy.py +0 -272
- agentsecure-0.1.1/agentsecure.egg-info/requires.txt +0 -3
- {agentsecure-0.1.1 → agentsecure-0.1.3}/LICENSE +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/NOTICE +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/__main__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/api/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/api/server.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/api/services.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/common.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/demo.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/policy.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/project.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/secrets.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cli/settings.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/client/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/client/wrappers.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/cloud.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/capabilities.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/command_metadata.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/config_profiles.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_mutation.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_ports.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_response.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/policy_validation.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/core/time.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/crypto/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/crypto/cipher.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/crypto/key_provider.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/commands.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/policies.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/sessions.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/daemon/supervisor.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/dotenv_scanner.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/env_scanner.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/patterns.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/scanner.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/discovery/suggestions.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/gateway/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/command.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/network.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/sanitizer.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/guard/wrappers.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/audit.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/encrypted_secret_store.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/grant_store.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/local_secret_store.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/policy.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/secret_store_factory.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/implementations/secrets.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/audit.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/grants.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/key_store.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/policy.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/interfaces/secrets.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/__init__.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/apply.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/materializer.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure/workspace/rewriter.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/dependency_links.txt +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/entry_points.txt +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/agentsecure.egg-info/top_level.txt +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/pyproject.toml +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/setup.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_audit.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_cli_demo.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_cli_policy.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_diff.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_discovery.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_encrypted_secret_store.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_env_policy.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_env_policy_contract.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_gateway_credentials.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_guard.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_guard_network.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_key_service.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_policy.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_policy_mutation.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_secrets.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_workspace.py +0 -0
- {agentsecure-0.1.1 → agentsecure-0.1.3}/tests/test_workspace_apply.py +0 -0
- {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.
|
|
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
|
|
@@ -33,7 +32,9 @@ Dynamic: license-file
|
|
|
33
32
|
[](https://github.com/ShellFrameAI/agentsecure-community/actions/workflows/ci.yml)
|
|
34
33
|
[](LICENSE)
|
|
35
34
|
|
|
36
|
-
|
|
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
|
[](https://github.com/ShellFrameAI/agentsecure-community/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
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,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
|
-
|
|
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
|
|
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
|
|
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()
|