agentsecure 0.1.0__py3-none-any.whl
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/__init__.py +4 -0
- agentsecure/__main__.py +5 -0
- agentsecure/api/__init__.py +1 -0
- agentsecure/api/server.py +8 -0
- agentsecure/api/services.py +3 -0
- agentsecure/cli/__init__.py +2 -0
- agentsecure/cli/common.py +116 -0
- agentsecure/cli/demo.py +97 -0
- agentsecure/cli/main.py +912 -0
- agentsecure/cli/policy.py +47 -0
- agentsecure/cli/project.py +137 -0
- agentsecure/cli/secrets.py +293 -0
- agentsecure/cli/settings.py +112 -0
- agentsecure/client/__init__.py +1 -0
- agentsecure/client/wrappers.py +87 -0
- agentsecure/cloud.py +30 -0
- agentsecure/core/__init__.py +2 -0
- agentsecure/core/capabilities.py +109 -0
- agentsecure/core/command_metadata.py +13 -0
- agentsecure/core/config.py +173 -0
- agentsecure/core/config_profiles.py +192 -0
- agentsecure/core/container.py +75 -0
- agentsecure/core/key_service.py +140 -0
- agentsecure/core/models.py +183 -0
- agentsecure/core/policy_mutation.py +127 -0
- agentsecure/core/policy_ports.py +61 -0
- agentsecure/core/policy_response.py +68 -0
- agentsecure/core/policy_validation.py +97 -0
- agentsecure/core/product.py +267 -0
- agentsecure/core/time.py +38 -0
- agentsecure/crypto/__init__.py +2 -0
- agentsecure/crypto/cipher.py +57 -0
- agentsecure/crypto/key_provider.py +39 -0
- agentsecure/daemon/__init__.py +1 -0
- agentsecure/daemon/commands.py +8 -0
- agentsecure/daemon/policies.py +3 -0
- agentsecure/daemon/sessions.py +3 -0
- agentsecure/daemon/supervisor.py +3 -0
- agentsecure/discovery/__init__.py +2 -0
- agentsecure/discovery/dotenv_scanner.py +60 -0
- agentsecure/discovery/env_scanner.py +29 -0
- agentsecure/discovery/patterns.py +90 -0
- agentsecure/discovery/scanner.py +27 -0
- agentsecure/discovery/suggestions.py +154 -0
- agentsecure/gateway/__init__.py +2 -0
- agentsecure/gateway/proxy.py +272 -0
- agentsecure/guard/__init__.py +2 -0
- agentsecure/guard/command.py +62 -0
- agentsecure/guard/network.py +92 -0
- agentsecure/guard/sanitizer.py +105 -0
- agentsecure/guard/wrappers.py +50 -0
- agentsecure/implementations/__init__.py +2 -0
- agentsecure/implementations/audit.py +53 -0
- agentsecure/implementations/encrypted_secret_store.py +73 -0
- agentsecure/implementations/grant_store.py +84 -0
- agentsecure/implementations/local_secret_store.py +53 -0
- agentsecure/implementations/policy.py +126 -0
- agentsecure/implementations/secret_store_factory.py +21 -0
- agentsecure/implementations/secrets.py +138 -0
- agentsecure/interfaces/__init__.py +2 -0
- agentsecure/interfaces/audit.py +11 -0
- agentsecure/interfaces/grants.py +23 -0
- agentsecure/interfaces/key_store.py +15 -0
- agentsecure/interfaces/policy.py +24 -0
- agentsecure/interfaces/secrets.py +19 -0
- agentsecure/workspace/__init__.py +2 -0
- agentsecure/workspace/apply.py +141 -0
- agentsecure/workspace/diff.py +104 -0
- agentsecure/workspace/materializer.py +112 -0
- agentsecure/workspace/rewriter.py +54 -0
- agentsecure/workspace/strategies.py +140 -0
- agentsecure-0.1.0.dist-info/METADATA +181 -0
- agentsecure-0.1.0.dist-info/RECORD +78 -0
- agentsecure-0.1.0.dist-info/WHEEL +5 -0
- agentsecure-0.1.0.dist-info/entry_points.txt +2 -0
- agentsecure-0.1.0.dist-info/licenses/LICENSE +68 -0
- agentsecure-0.1.0.dist-info/licenses/NOTICE +4 -0
- agentsecure-0.1.0.dist-info/top_level.txt +1 -0
agentsecure/cli/main.py
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import getpass
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import queue
|
|
6
|
+
import shutil
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
from urllib.request import Request, urlopen
|
|
15
|
+
|
|
16
|
+
from agentsecure.cloud import CloudError, CloudRuntimeService
|
|
17
|
+
from agentsecure.api.server import LocalApiServer
|
|
18
|
+
from agentsecure.api.services import ApiServices
|
|
19
|
+
from agentsecure.cli.common import (
|
|
20
|
+
cloud_features_disabled as _cloud_features_disabled,
|
|
21
|
+
cloud_features_enabled as _cloud_features_enabled,
|
|
22
|
+
scanner as _scanner,
|
|
23
|
+
)
|
|
24
|
+
from agentsecure.cli.demo import run_demo
|
|
25
|
+
from agentsecure.cli.policy import add_policy_subparser, handle_policy
|
|
26
|
+
from agentsecure.cli.project import (
|
|
27
|
+
_profile_label,
|
|
28
|
+
cleanup_project,
|
|
29
|
+
init_project,
|
|
30
|
+
run_doctor,
|
|
31
|
+
show_status,
|
|
32
|
+
uninstall_agentsecure,
|
|
33
|
+
)
|
|
34
|
+
from agentsecure.cli.secrets import (
|
|
35
|
+
discover_secrets,
|
|
36
|
+
handle_keys,
|
|
37
|
+
print_env,
|
|
38
|
+
protect_secrets,
|
|
39
|
+
suggest_policy,
|
|
40
|
+
)
|
|
41
|
+
from agentsecure.cli.settings import (
|
|
42
|
+
apply_workspace,
|
|
43
|
+
diff_workspace,
|
|
44
|
+
handle_files,
|
|
45
|
+
handle_network,
|
|
46
|
+
handle_setup,
|
|
47
|
+
)
|
|
48
|
+
from agentsecure.client.wrappers import AgentWrapperInstaller, SUPPORTED_AGENTS
|
|
49
|
+
from agentsecure.core.command_metadata import safe_command_metadata
|
|
50
|
+
from agentsecure.core.config import ConfigError, JsonConfigLoader, JsonConfigWriter
|
|
51
|
+
from agentsecure.core.config_profiles import (
|
|
52
|
+
profile_metadata_from_response,
|
|
53
|
+
profile_policy_body_from_response,
|
|
54
|
+
)
|
|
55
|
+
from agentsecure.core.capabilities import broker_url_for_env
|
|
56
|
+
from agentsecure.core.container import Container
|
|
57
|
+
from agentsecure.core.key_service import KeyManagementError, KeyManagementService
|
|
58
|
+
from agentsecure.core.models import AgentSecureConfig, DiscoveredSecret, ProcessRequest, SecretReplacement
|
|
59
|
+
from agentsecure.core.product import ProductService
|
|
60
|
+
from agentsecure.core.time import DurationError
|
|
61
|
+
from agentsecure.daemon.commands import CommandExecutor, CommandPoller
|
|
62
|
+
from agentsecure.daemon.policies import PolicyApplier
|
|
63
|
+
from agentsecure.daemon.sessions import SessionRegistry
|
|
64
|
+
from agentsecure.daemon.supervisor import AgentProcessSupervisor
|
|
65
|
+
from agentsecure.discovery.dotenv_scanner import DotenvSecretScanner
|
|
66
|
+
from agentsecure.discovery.env_scanner import EnvironmentSecretScanner
|
|
67
|
+
from agentsecure.discovery.patterns import mask_secret
|
|
68
|
+
from agentsecure.discovery.scanner import CompositeSecretScanner
|
|
69
|
+
from agentsecure.discovery.suggestions import PolicySuggestionService
|
|
70
|
+
from agentsecure.guard.command import GuardedCommandRunner
|
|
71
|
+
from agentsecure.guard.sanitizer import SecretOutputSanitizer
|
|
72
|
+
from agentsecure.guard.wrappers import CommandGuardWrapperInstaller
|
|
73
|
+
from agentsecure.gateway.proxy import LocalGateway
|
|
74
|
+
from agentsecure.implementations.audit import JsonLineAuditLogger
|
|
75
|
+
from agentsecure.implementations.grant_store import LocalJsonGrantStore
|
|
76
|
+
from agentsecure.implementations.secret_store_factory import encrypted_secret_store_for_config
|
|
77
|
+
from agentsecure.workspace.apply import WorkspaceApplier
|
|
78
|
+
from agentsecure.workspace.diff import WorkspaceDiff
|
|
79
|
+
from agentsecure.workspace.materializer import WorkspaceMaterializer, make_tree_writable
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
83
|
+
parser = build_parser()
|
|
84
|
+
args = parser.parse_args(argv)
|
|
85
|
+
if args.command == "run":
|
|
86
|
+
return run_agent(args)
|
|
87
|
+
if args.command == "gateway":
|
|
88
|
+
return run_gateway(args)
|
|
89
|
+
if args.command == "daemon":
|
|
90
|
+
return run_daemon(args)
|
|
91
|
+
if args.command == "env":
|
|
92
|
+
return print_env(args)
|
|
93
|
+
if args.command == "keys":
|
|
94
|
+
return handle_keys(args)
|
|
95
|
+
if args.command == "discover":
|
|
96
|
+
return discover_secrets(args)
|
|
97
|
+
if args.command == "suggest":
|
|
98
|
+
return suggest_policy(args)
|
|
99
|
+
if args.command == "protect":
|
|
100
|
+
result = protect_secrets(args)
|
|
101
|
+
return result if isinstance(result, int) else 0
|
|
102
|
+
if args.command == "api":
|
|
103
|
+
return run_api(args)
|
|
104
|
+
if args.command == "init":
|
|
105
|
+
return init_project(args)
|
|
106
|
+
if args.command == "status":
|
|
107
|
+
return show_status(args)
|
|
108
|
+
if args.command == "doctor":
|
|
109
|
+
return run_doctor(args)
|
|
110
|
+
if args.command == "cleanup":
|
|
111
|
+
return cleanup_project(args)
|
|
112
|
+
if args.command == "uninstall":
|
|
113
|
+
return uninstall_agentsecure(args)
|
|
114
|
+
if args.command == "files":
|
|
115
|
+
return handle_files(args)
|
|
116
|
+
if args.command == "network":
|
|
117
|
+
return handle_network(args)
|
|
118
|
+
if args.command == "policy":
|
|
119
|
+
return handle_policy(args)
|
|
120
|
+
if args.command == "setup":
|
|
121
|
+
return handle_setup(args)
|
|
122
|
+
if args.command == "enroll":
|
|
123
|
+
return enroll_cloud(args)
|
|
124
|
+
if args.command == "cloud":
|
|
125
|
+
return handle_cloud(args)
|
|
126
|
+
if args.command == "diff":
|
|
127
|
+
return diff_workspace(args)
|
|
128
|
+
if args.command == "apply":
|
|
129
|
+
return apply_workspace(args)
|
|
130
|
+
if args.command == "guard":
|
|
131
|
+
return guard_command(args)
|
|
132
|
+
if args.command == "demo":
|
|
133
|
+
return run_demo(args)
|
|
134
|
+
parser.print_help()
|
|
135
|
+
return 2
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
139
|
+
parser = argparse.ArgumentParser(prog="agentsecure")
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
"--config",
|
|
142
|
+
default="agentsecure.json",
|
|
143
|
+
help="Path to AgentSecure JSON config",
|
|
144
|
+
)
|
|
145
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
146
|
+
|
|
147
|
+
run_parser = subparsers.add_parser("run", help="Run an agent under AgentSecure")
|
|
148
|
+
run_parser.add_argument("--no-discover", action="store_true", help="Skip pre-run secret discovery prompt")
|
|
149
|
+
run_parser.add_argument("--protect-all", action="store_true", default=True, help="Virtualize all discovered secrets without prompting")
|
|
150
|
+
run_parser.add_argument("--prompt-secrets", dest="protect_all", action="store_false", help="Prompt before virtualizing discovered secrets")
|
|
151
|
+
run_parser.add_argument(
|
|
152
|
+
"--runtime",
|
|
153
|
+
choices=["workspace", "command-guard"],
|
|
154
|
+
default="command-guard",
|
|
155
|
+
help="Runtime mode. command-guard runs in place and sanitizes common read commands; workspace materializes sanitized files.",
|
|
156
|
+
)
|
|
157
|
+
run_parser.add_argument("--no-workspace", action="store_true", help="Run in the real project directory")
|
|
158
|
+
run_parser.add_argument("--workspace-keep", action="store_true", help="Keep the safe workspace after the agent exits")
|
|
159
|
+
run_parser.add_argument("--read-only-workspace", action="store_true", help="Make the safe workspace read-only")
|
|
160
|
+
run_parser.add_argument("--no-new-files", action="store_true", help="Block creating, deleting, or renaming files in the safe workspace")
|
|
161
|
+
run_parser.add_argument(
|
|
162
|
+
"--workspace-mode",
|
|
163
|
+
choices=["symlink", "copy"],
|
|
164
|
+
default="symlink",
|
|
165
|
+
help="Workspace strategy. symlink is fast and lets normal edits hit the real project; copy is safer review mode.",
|
|
166
|
+
)
|
|
167
|
+
run_parser.add_argument("--ttl", default="2h", help="Grant duration for protected discovered secrets")
|
|
168
|
+
run_parser.add_argument("agent_command", nargs=argparse.REMAINDER)
|
|
169
|
+
|
|
170
|
+
subparsers.add_parser("gateway", help="Run only the local gateway")
|
|
171
|
+
subparsers.add_parser("env", help="Print virtual environment variables")
|
|
172
|
+
|
|
173
|
+
keys_parser = subparsers.add_parser("keys", help="Manage virtual keys")
|
|
174
|
+
keys_subparsers = keys_parser.add_subparsers(dest="keys_command")
|
|
175
|
+
create_parser = keys_subparsers.add_parser("create", help="Create a virtual key")
|
|
176
|
+
create_parser.add_argument("--env-name", required=True, help="Agent-visible environment variable name")
|
|
177
|
+
create_parser.add_argument("--provider", default="custom", help="Provider label, such as openai")
|
|
178
|
+
create_parser.add_argument("--inject-as", default="authorization_bearer", help="Credential injection mode")
|
|
179
|
+
create_parser.add_argument("--name", default="", help="Optional human-readable key name")
|
|
180
|
+
create_parser.add_argument("--real-secret-env", help="Read the real secret from this local environment variable")
|
|
181
|
+
create_parser.add_argument("--real-secret-stdin", action="store_true", help="Read the real secret from stdin")
|
|
182
|
+
create_parser.add_argument("--ttl", default="2h", help="Grant duration, default 2h, max 24h")
|
|
183
|
+
keys_subparsers.add_parser("list", help="List virtual key grants")
|
|
184
|
+
revoke_parser = keys_subparsers.add_parser("revoke", help="Revoke a virtual key")
|
|
185
|
+
revoke_parser.add_argument("virtual_token")
|
|
186
|
+
|
|
187
|
+
subparsers.add_parser("discover", help="Discover likely local secrets")
|
|
188
|
+
subparsers.add_parser("suggest", help="Suggest env and network policy for discovered secrets")
|
|
189
|
+
protect_parser = subparsers.add_parser("protect", help="Interactively virtualize discovered secrets")
|
|
190
|
+
protect_parser.add_argument("--protect-all", action="store_true", help="Virtualize all discovered secrets without prompting")
|
|
191
|
+
protect_parser.add_argument("--ttl", default="2h", help="Grant duration, default 2h, max 24h")
|
|
192
|
+
|
|
193
|
+
init_parser = subparsers.add_parser("init", help="Initialize AgentSecure in this project")
|
|
194
|
+
init_parser.add_argument("--force", action="store_true", help="Overwrite existing AgentSecure config")
|
|
195
|
+
init_parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
196
|
+
|
|
197
|
+
status_parser = subparsers.add_parser("status", help="Show AgentSecure project status")
|
|
198
|
+
status_parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
199
|
+
|
|
200
|
+
doctor_parser = subparsers.add_parser("doctor", help="Check AgentSecure project setup")
|
|
201
|
+
doctor_parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
202
|
+
|
|
203
|
+
cleanup_parser = subparsers.add_parser("cleanup", help="Remove local AgentSecure trial state from this project")
|
|
204
|
+
cleanup_parser.add_argument("--yes", action="store_true", help="Confirm removal without prompting")
|
|
205
|
+
cleanup_parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
206
|
+
|
|
207
|
+
uninstall_parser = subparsers.add_parser("uninstall", help="Remove AgentSecure from this project and user bin")
|
|
208
|
+
uninstall_parser.add_argument("--yes", action="store_true", help="Confirm removal without prompting")
|
|
209
|
+
uninstall_parser.add_argument("--install-dir", default=os.path.expanduser("~/.agentsecure/bin"), help="AgentSecure user bin directory")
|
|
210
|
+
|
|
211
|
+
files_parser = subparsers.add_parser("files", help="Manage write-protected files")
|
|
212
|
+
files_subparsers = files_parser.add_subparsers(dest="files_command")
|
|
213
|
+
files_subparsers.add_parser("list", help="List paths protected from writes in safe workspaces")
|
|
214
|
+
protect_file_parser = files_subparsers.add_parser("protect", help="Protect paths from writes")
|
|
215
|
+
protect_file_parser.add_argument("paths", nargs="+")
|
|
216
|
+
unprotect_file_parser = files_subparsers.add_parser("unprotect", help="Remove write protection for paths")
|
|
217
|
+
unprotect_file_parser.add_argument("paths", nargs="+")
|
|
218
|
+
|
|
219
|
+
network_parser = subparsers.add_parser("network", help="Manage network allowlist")
|
|
220
|
+
network_subparsers = network_parser.add_subparsers(dest="network_command")
|
|
221
|
+
network_subparsers.add_parser("list", help="List allowed credential destinations")
|
|
222
|
+
network_allow_parser = network_subparsers.add_parser("allow", help="Allow credential use for domains")
|
|
223
|
+
network_allow_parser.add_argument("domains", nargs="+")
|
|
224
|
+
network_remove_parser = network_subparsers.add_parser("remove", help="Remove domains from credential allowlist")
|
|
225
|
+
network_remove_parser.add_argument("domains", nargs="+")
|
|
226
|
+
|
|
227
|
+
add_policy_subparser(subparsers)
|
|
228
|
+
|
|
229
|
+
setup_parser = subparsers.add_parser("setup", help="Install local protected agent command wrappers")
|
|
230
|
+
setup_parser.add_argument("--bin-dir", default=os.path.expanduser("~/.agentsecure/bin"), help="Directory for wrapper commands")
|
|
231
|
+
setup_subparsers = setup_parser.add_subparsers(dest="setup_command")
|
|
232
|
+
setup_install_parser = setup_subparsers.add_parser("install", help="Install wrapper commands")
|
|
233
|
+
setup_install_parser.add_argument("agents", nargs="+", choices=SUPPORTED_AGENTS)
|
|
234
|
+
setup_remove_parser = setup_subparsers.add_parser("remove", help="Remove wrapper commands")
|
|
235
|
+
setup_remove_parser.add_argument("agents", nargs="+", choices=SUPPORTED_AGENTS)
|
|
236
|
+
setup_subparsers.add_parser("list", help="List wrapper commands")
|
|
237
|
+
|
|
238
|
+
diff_parser = subparsers.add_parser("diff", help="Show changes in a kept safe workspace")
|
|
239
|
+
diff_parser.add_argument("--workspace", help="Workspace path. Defaults to latest kept workspace")
|
|
240
|
+
diff_parser.add_argument("--include-protected", action="store_true", help="Include protected files such as .env")
|
|
241
|
+
|
|
242
|
+
apply_parser = subparsers.add_parser("apply", help="Apply safe changes from a kept workspace")
|
|
243
|
+
apply_parser.add_argument("--workspace", help="Workspace path. Defaults to latest kept workspace")
|
|
244
|
+
apply_parser.add_argument("--dry-run", action="store_true", help="Show what would be applied without copying files")
|
|
245
|
+
|
|
246
|
+
guard_parser = subparsers.add_parser("guard", help="Run the local command-guard wrapper")
|
|
247
|
+
guard_parser.add_argument("tool")
|
|
248
|
+
guard_parser.add_argument("tool_args", nargs=argparse.REMAINDER)
|
|
249
|
+
|
|
250
|
+
demo_parser = subparsers.add_parser("demo", help="Run a local-only community .env masking demo")
|
|
251
|
+
demo_parser.add_argument("--keep", action="store_true", help="Keep the temporary demo project")
|
|
252
|
+
return parser
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def run_agent(args: argparse.Namespace) -> int:
|
|
256
|
+
cloud = CloudRuntimeService() if _cloud_features_enabled() else None
|
|
257
|
+
if cloud:
|
|
258
|
+
_apply_cloud_runtime_defaults(args, cloud)
|
|
259
|
+
_pull_cloud_policy(args.config, cloud)
|
|
260
|
+
project_name = getattr(args, "project", "") or os.path.basename(os.getcwd()) or "default"
|
|
261
|
+
task_label = getattr(args, "task", "") or "Untitled session"
|
|
262
|
+
replacements = []
|
|
263
|
+
if not args.no_discover:
|
|
264
|
+
replacements = protect_secrets(args)
|
|
265
|
+
if isinstance(replacements, int):
|
|
266
|
+
return replacements
|
|
267
|
+
container = Container.from_config_path(args.config)
|
|
268
|
+
run_cwd = os.getcwd()
|
|
269
|
+
workspace_session = None
|
|
270
|
+
materializer = WorkspaceMaterializer()
|
|
271
|
+
should_create_workspace = bool(replacements or container.config.files.protect_write)
|
|
272
|
+
if args.runtime == "command-guard":
|
|
273
|
+
print("AgentSecure runtime: command-guard", flush=True)
|
|
274
|
+
print("No workspace created. Common secret reads are sanitized through command wrappers.", flush=True)
|
|
275
|
+
elif should_create_workspace and not args.no_workspace:
|
|
276
|
+
try:
|
|
277
|
+
workspace_mode = "copy" if args.read_only_workspace else args.workspace_mode
|
|
278
|
+
workspace_session = materializer.create_workspace(
|
|
279
|
+
run_cwd,
|
|
280
|
+
replacements,
|
|
281
|
+
args.ttl,
|
|
282
|
+
mode=workspace_mode,
|
|
283
|
+
protected_write_paths=container.config.files.protect_write,
|
|
284
|
+
)
|
|
285
|
+
if args.read_only_workspace:
|
|
286
|
+
materializer.make_read_only(workspace_session.workspace_root)
|
|
287
|
+
else:
|
|
288
|
+
materializer.protect_write_paths(
|
|
289
|
+
workspace_session.workspace_root,
|
|
290
|
+
container.config.files.protect_write,
|
|
291
|
+
)
|
|
292
|
+
if args.no_new_files:
|
|
293
|
+
materializer.prevent_new_files(workspace_session.workspace_root)
|
|
294
|
+
run_cwd = workspace_session.workspace_root
|
|
295
|
+
print("AgentSecure safe workspace: %s" % workspace_session.workspace_root, flush=True)
|
|
296
|
+
print("AgentSecure workspace mode: %s" % workspace_session.mode, flush=True)
|
|
297
|
+
if workspace_session.mode == "symlink":
|
|
298
|
+
print("Secrets are virtualized. Normal file edits may affect the real project.", flush=True)
|
|
299
|
+
else:
|
|
300
|
+
print("Real project files were not modified.", flush=True)
|
|
301
|
+
if args.read_only_workspace:
|
|
302
|
+
print("Safe workspace is read-only.", flush=True)
|
|
303
|
+
elif args.no_new_files:
|
|
304
|
+
print("Safe workspace blocks new files.", flush=True)
|
|
305
|
+
except (DurationError, OSError) as exc:
|
|
306
|
+
sys.stderr.write("agentsecure: failed to create safe workspace: %s\n" % exc)
|
|
307
|
+
return 1
|
|
308
|
+
argv = list(args.agent_command)
|
|
309
|
+
if argv and argv[0] == "--":
|
|
310
|
+
argv = argv[1:]
|
|
311
|
+
argv = _apply_read_only_agent_mode(argv, args.read_only_workspace)
|
|
312
|
+
decision = container.policy_engine.evaluate_process(ProcessRequest(argv=argv, cwd=run_cwd))
|
|
313
|
+
if not decision.allowed:
|
|
314
|
+
sys.stderr.write("agentsecure: blocked process: " + decision.reason + "\n")
|
|
315
|
+
return 126
|
|
316
|
+
if not argv:
|
|
317
|
+
sys.stderr.write("agentsecure: missing agent command\n")
|
|
318
|
+
return 2
|
|
319
|
+
|
|
320
|
+
daemon = _running_daemon()
|
|
321
|
+
daemon_session = None
|
|
322
|
+
gateway_thread = None
|
|
323
|
+
if daemon:
|
|
324
|
+
daemon_session = _daemon_create_session(
|
|
325
|
+
daemon,
|
|
326
|
+
{
|
|
327
|
+
"agent": os.path.basename(argv[0]) if argv else "",
|
|
328
|
+
"argv": argv,
|
|
329
|
+
"project": project_name,
|
|
330
|
+
"task": task_label,
|
|
331
|
+
"runtime": args.runtime,
|
|
332
|
+
"cwd": run_cwd,
|
|
333
|
+
"config_profile": cloud.config_profile() if cloud else {},
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
gateway_host = str(daemon.get("gateway_host", container.config.gateway.host))
|
|
337
|
+
gateway_port = int(daemon.get("gateway_port", container.config.gateway.port))
|
|
338
|
+
else:
|
|
339
|
+
gateway_host = container.config.gateway.host
|
|
340
|
+
gateway_port = _available_gateway_port(gateway_host, container.config.gateway.port)
|
|
341
|
+
gateway_thread = _start_local_gateway_thread(container, gateway_host, gateway_port)
|
|
342
|
+
if isinstance(gateway_thread, int):
|
|
343
|
+
return gateway_thread
|
|
344
|
+
|
|
345
|
+
env = os.environ.copy()
|
|
346
|
+
for env_name in container.config.env_policy.rules:
|
|
347
|
+
env.pop(env_name, None)
|
|
348
|
+
env.update(container.virtual_env_provider.build_environment())
|
|
349
|
+
if args.runtime == "command-guard":
|
|
350
|
+
CommandGuardWrapperInstaller(args.config).install(env)
|
|
351
|
+
session_id = daemon_session.get("session_id", "") if daemon_session else ""
|
|
352
|
+
if session_id:
|
|
353
|
+
env["AGENTSECURE_SESSION_ID"] = session_id
|
|
354
|
+
proxy_url = _proxy_url(gateway_host, gateway_port, session_id)
|
|
355
|
+
_apply_proxy_environment(env, proxy_url)
|
|
356
|
+
command_metadata = safe_command_metadata(argv)
|
|
357
|
+
container.audit_logger.record(
|
|
358
|
+
"agent_started",
|
|
359
|
+
{
|
|
360
|
+
"argv": command_metadata["argv"],
|
|
361
|
+
"argc": command_metadata["argc"],
|
|
362
|
+
"proxy": proxy_url,
|
|
363
|
+
"cwd": run_cwd,
|
|
364
|
+
"workspace": workspace_session.workspace_root if workspace_session else "",
|
|
365
|
+
"project": project_name,
|
|
366
|
+
"task": task_label,
|
|
367
|
+
"session_id": session_id,
|
|
368
|
+
"daemon": bool(daemon),
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
cloud_session = None
|
|
372
|
+
cloud_stop = threading.Event()
|
|
373
|
+
cloud_thread = None
|
|
374
|
+
command_poller = None
|
|
375
|
+
command_executor = None
|
|
376
|
+
if cloud and cloud.status().get("enrolled"):
|
|
377
|
+
cloud_session = cloud.session_payload(
|
|
378
|
+
argv,
|
|
379
|
+
project_name,
|
|
380
|
+
task_label,
|
|
381
|
+
args.runtime,
|
|
382
|
+
run_cwd,
|
|
383
|
+
workspace_session.workspace_root if workspace_session else "",
|
|
384
|
+
config_profile=cloud.config_profile(),
|
|
385
|
+
)
|
|
386
|
+
if session_id:
|
|
387
|
+
cloud_session["session_id"] = session_id
|
|
388
|
+
if getattr(args, "cloud_debug", False):
|
|
389
|
+
os.environ["AGENTSECURE_CLOUD_DEBUG"] = "true"
|
|
390
|
+
session_finished = False
|
|
391
|
+
try:
|
|
392
|
+
process = _start_agent_process(argv, env, run_cwd)
|
|
393
|
+
process_group_id = _process_group_id(process)
|
|
394
|
+
if cloud_session and cloud and cloud.status().get("enrolled"):
|
|
395
|
+
command_executor = _run_command_executor(
|
|
396
|
+
args.config,
|
|
397
|
+
container.audit_logger,
|
|
398
|
+
cloud_session,
|
|
399
|
+
process.pid,
|
|
400
|
+
)
|
|
401
|
+
start_response = _try_cloud_sync(cloud, cloud_session, "running", force=True)
|
|
402
|
+
_execute_cloud_response_commands(cloud, command_executor, start_response)
|
|
403
|
+
cloud_thread = _start_cloud_report_thread(cloud, cloud_session, cloud_stop, command_executor)
|
|
404
|
+
if daemon and daemon_session:
|
|
405
|
+
_daemon_update_session(
|
|
406
|
+
daemon,
|
|
407
|
+
session_id,
|
|
408
|
+
{
|
|
409
|
+
"pid": process.pid,
|
|
410
|
+
"pgid": os.getpgid(process.pid) if os.name == "posix" else process.pid,
|
|
411
|
+
"status": "running",
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
elif command_executor:
|
|
415
|
+
command_poller = CommandPoller(cloud, command_executor)
|
|
416
|
+
command_poller.start()
|
|
417
|
+
exit_code = process.wait()
|
|
418
|
+
if exit_code is None or exit_code >= 0:
|
|
419
|
+
_wait_for_process_group_exit(process_group_id)
|
|
420
|
+
final_status = "killed" if exit_code is not None and exit_code < 0 else "finished"
|
|
421
|
+
if daemon and daemon_session:
|
|
422
|
+
_daemon_finish_session(daemon, session_id, final_status, exit_code)
|
|
423
|
+
session_finished = True
|
|
424
|
+
if cloud_session:
|
|
425
|
+
container.audit_logger.record(
|
|
426
|
+
"agent_killed" if final_status == "killed" else "agent_finished",
|
|
427
|
+
{
|
|
428
|
+
"session_id": cloud_session.get("session_id", ""),
|
|
429
|
+
"project": project_name,
|
|
430
|
+
"task": task_label,
|
|
431
|
+
"exit_code": exit_code,
|
|
432
|
+
},
|
|
433
|
+
)
|
|
434
|
+
finish_response = _try_cloud_sync(cloud, cloud_session, final_status, exit_code, force=True)
|
|
435
|
+
if command_executor:
|
|
436
|
+
_execute_cloud_response_commands(cloud, command_executor, finish_response)
|
|
437
|
+
return exit_code
|
|
438
|
+
finally:
|
|
439
|
+
if command_poller:
|
|
440
|
+
command_poller.stop()
|
|
441
|
+
if daemon and daemon_session and not session_finished:
|
|
442
|
+
_daemon_finish_session(daemon, session_id, "finished", None)
|
|
443
|
+
cloud_stop.set()
|
|
444
|
+
if cloud_thread:
|
|
445
|
+
cloud_thread.join(timeout=2)
|
|
446
|
+
if workspace_session and not args.workspace_keep:
|
|
447
|
+
materializer.make_writable(workspace_session.workspace_root)
|
|
448
|
+
shutil.rmtree(workspace_session.workspace_root, ignore_errors=True)
|
|
449
|
+
container.audit_logger.record(
|
|
450
|
+
"workspace_removed",
|
|
451
|
+
{"workspace": workspace_session.workspace_root},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _start_agent_process(argv: List[str], env, cwd: str):
|
|
456
|
+
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)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _process_group_id(process) -> int:
|
|
462
|
+
if os.name != "posix":
|
|
463
|
+
return 0
|
|
464
|
+
try:
|
|
465
|
+
return os.getpgid(process.pid)
|
|
466
|
+
except OSError:
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _wait_for_process_group_exit(process_group_id: int) -> None:
|
|
471
|
+
if os.name != "posix" or not process_group_id:
|
|
472
|
+
return
|
|
473
|
+
while _process_group_alive(process_group_id):
|
|
474
|
+
time.sleep(0.5)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _process_group_alive(process_group_id: int) -> bool:
|
|
478
|
+
try:
|
|
479
|
+
os.killpg(process_group_id, 0)
|
|
480
|
+
return True
|
|
481
|
+
except ProcessLookupError:
|
|
482
|
+
return False
|
|
483
|
+
except PermissionError:
|
|
484
|
+
return True
|
|
485
|
+
except OSError:
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _run_command_executor(
|
|
490
|
+
config_path: str,
|
|
491
|
+
audit_logger: JsonLineAuditLogger,
|
|
492
|
+
cloud_session,
|
|
493
|
+
pid: int,
|
|
494
|
+
):
|
|
495
|
+
session_id = str(cloud_session.get("session_id", ""))
|
|
496
|
+
if not session_id:
|
|
497
|
+
return None
|
|
498
|
+
project_root = os.path.dirname(os.path.abspath(config_path)) or os.getcwd()
|
|
499
|
+
sessions = SessionRegistry(project_root)
|
|
500
|
+
supervisor = AgentProcessSupervisor(sessions)
|
|
501
|
+
supervisor.attach_process(session_id, pid)
|
|
502
|
+
executor = CommandExecutor(
|
|
503
|
+
supervisor,
|
|
504
|
+
PolicyApplier(config_path),
|
|
505
|
+
config_path,
|
|
506
|
+
audit_logger,
|
|
507
|
+
)
|
|
508
|
+
return executor
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _apply_cloud_runtime_defaults(args: argparse.Namespace, cloud: CloudRuntimeService) -> None:
|
|
512
|
+
defaults = cloud.runtime_defaults()
|
|
513
|
+
if not defaults:
|
|
514
|
+
return
|
|
515
|
+
if "protect_all_by_default" in defaults:
|
|
516
|
+
args.protect_all = bool(defaults.get("protect_all_by_default"))
|
|
517
|
+
runtime_mode = defaults.get("runtime_mode")
|
|
518
|
+
if runtime_mode in ("command-guard", "workspace"):
|
|
519
|
+
args.runtime = runtime_mode
|
|
520
|
+
workspace_mode = defaults.get("workspace_mode")
|
|
521
|
+
if workspace_mode in ("symlink", "copy"):
|
|
522
|
+
args.workspace_mode = workspace_mode
|
|
523
|
+
if defaults.get("reporting_debug"):
|
|
524
|
+
args.cloud_debug = True
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _pull_cloud_policy(config_path: str, cloud: CloudRuntimeService) -> bool:
|
|
528
|
+
if not cloud.status().get("enrolled"):
|
|
529
|
+
return False
|
|
530
|
+
try:
|
|
531
|
+
response = cloud.sync(session={}, status="idle", force=True)
|
|
532
|
+
except CloudError:
|
|
533
|
+
return False
|
|
534
|
+
try:
|
|
535
|
+
return _apply_cloud_policy_response(config_path, response)
|
|
536
|
+
except (OSError, ValueError, TypeError):
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _apply_cloud_policy_response(config_path: str, response) -> bool:
|
|
541
|
+
if not isinstance(response, dict):
|
|
542
|
+
return False
|
|
543
|
+
policy = response.get("policy", {}) if isinstance(response.get("policy", {}), dict) else {}
|
|
544
|
+
profile_body = profile_policy_body_from_response(response)
|
|
545
|
+
selected_policy = policy if policy else profile_body
|
|
546
|
+
selected_profile = profile_metadata_from_response(
|
|
547
|
+
response,
|
|
548
|
+
source="sync",
|
|
549
|
+
)
|
|
550
|
+
if not selected_policy and not selected_profile:
|
|
551
|
+
return False
|
|
552
|
+
current = {}
|
|
553
|
+
if os.path.exists(config_path):
|
|
554
|
+
with open(config_path, "r") as handle:
|
|
555
|
+
loaded = json.load(handle)
|
|
556
|
+
current = loaded if isinstance(loaded, dict) else {}
|
|
557
|
+
version = _cloud_policy_version(response, selected_profile)
|
|
558
|
+
PolicyApplier(config_path).apply(current, selected_policy, version, selected_profile)
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _cloud_policy_version(response: Dict[str, Any], selected_profile: Dict[str, Any]) -> int:
|
|
563
|
+
for key in ("policy_version", "version", "profile_version"):
|
|
564
|
+
try:
|
|
565
|
+
value = int(response.get(key, 0) or 0)
|
|
566
|
+
except (TypeError, ValueError):
|
|
567
|
+
value = 0
|
|
568
|
+
if value > 0:
|
|
569
|
+
return value
|
|
570
|
+
try:
|
|
571
|
+
return int(selected_profile.get("version", 0) or 0)
|
|
572
|
+
except (TypeError, ValueError):
|
|
573
|
+
return 0
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _start_local_gateway_thread(container: Container, host: str, port: int):
|
|
577
|
+
gateway_events = queue.Queue()
|
|
578
|
+
gateway = LocalGateway(
|
|
579
|
+
host,
|
|
580
|
+
port,
|
|
581
|
+
container.policy_engine,
|
|
582
|
+
container.token_resolver,
|
|
583
|
+
container.audit_logger,
|
|
584
|
+
container.bindings,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
def run_gateway_thread() -> None:
|
|
588
|
+
try:
|
|
589
|
+
gateway.serve_forever(lambda: gateway_events.put("ready"))
|
|
590
|
+
except Exception as exc:
|
|
591
|
+
gateway_events.put(exc)
|
|
592
|
+
|
|
593
|
+
gateway_thread = threading.Thread(target=run_gateway_thread)
|
|
594
|
+
gateway_thread.daemon = True
|
|
595
|
+
gateway_thread.start()
|
|
596
|
+
try:
|
|
597
|
+
gateway_status = gateway_events.get(timeout=5)
|
|
598
|
+
except queue.Empty:
|
|
599
|
+
sys.stderr.write("agentsecure: gateway did not start within 5 seconds\n")
|
|
600
|
+
return 1
|
|
601
|
+
if isinstance(gateway_status, Exception):
|
|
602
|
+
sys.stderr.write("agentsecure: gateway failed to start: %s\n" % gateway_status)
|
|
603
|
+
return 1
|
|
604
|
+
return gateway_thread
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _proxy_url(host: str, port: int, session_id: str = "") -> str:
|
|
608
|
+
if session_id:
|
|
609
|
+
return "http://%s@%s:%s" % (session_id, host, port)
|
|
610
|
+
return "http://%s:%s" % (host, port)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _available_gateway_port(host: str, preferred_port: int) -> int:
|
|
614
|
+
for port in [preferred_port] + list(range(8766, 8899)):
|
|
615
|
+
if _can_bind(host, port):
|
|
616
|
+
return port
|
|
617
|
+
return preferred_port
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _can_bind(host: str, port: int) -> bool:
|
|
621
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
622
|
+
try:
|
|
623
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
624
|
+
sock.bind((host, port))
|
|
625
|
+
return True
|
|
626
|
+
except OSError:
|
|
627
|
+
return False
|
|
628
|
+
finally:
|
|
629
|
+
sock.close()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _running_daemon():
|
|
633
|
+
state = _read_daemon_state()
|
|
634
|
+
if not state:
|
|
635
|
+
return None
|
|
636
|
+
try:
|
|
637
|
+
daemon = _http_json("GET", _daemon_url(state, "/daemon"))
|
|
638
|
+
except Exception:
|
|
639
|
+
return None
|
|
640
|
+
if daemon.get("running"):
|
|
641
|
+
return daemon
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _read_daemon_state():
|
|
646
|
+
path = os.path.join(".agentsecure", "daemon.json")
|
|
647
|
+
if not os.path.exists(path):
|
|
648
|
+
return None
|
|
649
|
+
try:
|
|
650
|
+
with open(path, "r") as handle:
|
|
651
|
+
data = json.load(handle)
|
|
652
|
+
except (ValueError, OSError):
|
|
653
|
+
return None
|
|
654
|
+
return data if isinstance(data, dict) else None
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _write_daemon_state(state) -> None:
|
|
658
|
+
os.makedirs(".agentsecure", exist_ok=True)
|
|
659
|
+
with open(os.path.join(".agentsecure", "daemon.json"), "w") as handle:
|
|
660
|
+
json.dump(state, handle, indent=2, sort_keys=True)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _daemon_url(state, path: str) -> str:
|
|
664
|
+
return "http://%s:%s%s" % (state.get("api_host", "127.0.0.1"), state.get("api_port", 8787), path)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _daemon_create_session(daemon, payload):
|
|
668
|
+
return _http_json("POST", _daemon_url(daemon, "/sessions"), payload)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _daemon_finish_session(daemon, session_id: str, status: str, exit_code) -> None:
|
|
672
|
+
if not session_id:
|
|
673
|
+
return
|
|
674
|
+
try:
|
|
675
|
+
_http_json(
|
|
676
|
+
"POST",
|
|
677
|
+
_daemon_url(daemon, "/sessions/finish"),
|
|
678
|
+
{"session_id": session_id, "status": status, "exit_code": exit_code},
|
|
679
|
+
)
|
|
680
|
+
except Exception:
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _daemon_update_session(daemon, session_id: str, fields) -> None:
|
|
685
|
+
if not session_id:
|
|
686
|
+
return
|
|
687
|
+
try:
|
|
688
|
+
_http_json(
|
|
689
|
+
"POST",
|
|
690
|
+
_daemon_url(daemon, "/sessions/update"),
|
|
691
|
+
{"session_id": session_id, "fields": fields},
|
|
692
|
+
)
|
|
693
|
+
except Exception:
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _http_json(method: str, url: str, payload=None):
|
|
698
|
+
data = None
|
|
699
|
+
if payload is not None:
|
|
700
|
+
data = json.dumps(payload).encode("utf-8")
|
|
701
|
+
request = Request(
|
|
702
|
+
url,
|
|
703
|
+
data=data,
|
|
704
|
+
headers={"Content-Type": "application/json"},
|
|
705
|
+
method=method,
|
|
706
|
+
)
|
|
707
|
+
with urlopen(request, timeout=2) as response:
|
|
708
|
+
raw = response.read().decode("utf-8")
|
|
709
|
+
return json.loads(raw) if raw else {}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _start_cloud_report_thread(cloud: CloudRuntimeService, session, stop_event, command_executor=None):
|
|
713
|
+
def run() -> None:
|
|
714
|
+
while not stop_event.is_set():
|
|
715
|
+
if cloud.has_reportable_events():
|
|
716
|
+
response = _try_cloud_sync(cloud, session, "running")
|
|
717
|
+
if command_executor:
|
|
718
|
+
_execute_cloud_response_commands(cloud, command_executor, response)
|
|
719
|
+
interval = 5 if cloud.status().get("debug_reporting") else 15
|
|
720
|
+
stop_event.wait(interval)
|
|
721
|
+
|
|
722
|
+
thread = threading.Thread(target=run)
|
|
723
|
+
thread.daemon = True
|
|
724
|
+
thread.start()
|
|
725
|
+
return thread
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _execute_cloud_response_commands(cloud: CloudRuntimeService, executor: CommandExecutor, response) -> None:
|
|
729
|
+
if not isinstance(response, dict):
|
|
730
|
+
return
|
|
731
|
+
commands = response.get("commands", [])
|
|
732
|
+
if not isinstance(commands, list):
|
|
733
|
+
return
|
|
734
|
+
for command in commands:
|
|
735
|
+
if not isinstance(command, dict):
|
|
736
|
+
continue
|
|
737
|
+
result = executor.execute(command)
|
|
738
|
+
command_id = str(command.get("id", ""))
|
|
739
|
+
if command_id:
|
|
740
|
+
try:
|
|
741
|
+
cloud.command_result(command_id, result)
|
|
742
|
+
except CloudError:
|
|
743
|
+
pass
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _try_cloud_sync(
|
|
747
|
+
cloud: CloudRuntimeService,
|
|
748
|
+
session,
|
|
749
|
+
status: str,
|
|
750
|
+
exit_code: Optional[int] = None,
|
|
751
|
+
force: bool = False,
|
|
752
|
+
) -> None:
|
|
753
|
+
try:
|
|
754
|
+
return cloud.sync(session=session, status=status, exit_code=exit_code, force=force)
|
|
755
|
+
except CloudError:
|
|
756
|
+
return {}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _apply_proxy_environment(env, proxy_url: str) -> None:
|
|
760
|
+
env["HTTP_PROXY"] = proxy_url
|
|
761
|
+
env["HTTPS_PROXY"] = proxy_url
|
|
762
|
+
env["http_proxy"] = proxy_url
|
|
763
|
+
env["https_proxy"] = proxy_url
|
|
764
|
+
no_proxy = _merge_no_proxy(env.get("NO_PROXY") or env.get("no_proxy") or "")
|
|
765
|
+
env["NO_PROXY"] = no_proxy
|
|
766
|
+
env["no_proxy"] = no_proxy
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _merge_no_proxy(existing: str) -> str:
|
|
770
|
+
defaults = [
|
|
771
|
+
"localhost",
|
|
772
|
+
"127.0.0.1",
|
|
773
|
+
"::1",
|
|
774
|
+
"0.0.0.0",
|
|
775
|
+
".local",
|
|
776
|
+
"host.docker.internal",
|
|
777
|
+
]
|
|
778
|
+
values = []
|
|
779
|
+
seen = set()
|
|
780
|
+
for raw in existing.split(",") + defaults:
|
|
781
|
+
value = raw.strip()
|
|
782
|
+
if not value:
|
|
783
|
+
continue
|
|
784
|
+
key = value.lower()
|
|
785
|
+
if key not in seen:
|
|
786
|
+
values.append(value)
|
|
787
|
+
seen.add(key)
|
|
788
|
+
return ",".join(values)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def run_gateway(args: argparse.Namespace) -> int:
|
|
792
|
+
container = Container.from_config_path(args.config)
|
|
793
|
+
container.gateway().serve_forever()
|
|
794
|
+
return 0
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def run_daemon(args: argparse.Namespace) -> int:
|
|
798
|
+
if args.host not in ("127.0.0.1", "localhost"):
|
|
799
|
+
sys.stderr.write("agentsecure: daemon must bind to localhost\n")
|
|
800
|
+
return 2
|
|
801
|
+
host = "127.0.0.1" if args.host == "localhost" else args.host
|
|
802
|
+
container = Container.from_config_path(args.config)
|
|
803
|
+
gateway_port = _available_gateway_port(host, args.gateway_port)
|
|
804
|
+
daemon_info = {
|
|
805
|
+
"api_host": host,
|
|
806
|
+
"api_port": args.api_port,
|
|
807
|
+
"gateway_host": host,
|
|
808
|
+
"gateway_port": gateway_port,
|
|
809
|
+
"started_at": time.time(),
|
|
810
|
+
}
|
|
811
|
+
gateway_thread = _start_local_gateway_thread(container, host, gateway_port)
|
|
812
|
+
if isinstance(gateway_thread, int):
|
|
813
|
+
return gateway_thread
|
|
814
|
+
_write_daemon_state(daemon_info)
|
|
815
|
+
services = ApiServices(args.config, _scanner(), daemon_info=daemon_info)
|
|
816
|
+
sessions = SessionRegistry(os.getcwd())
|
|
817
|
+
supervisor = AgentProcessSupervisor(sessions)
|
|
818
|
+
executor = CommandExecutor(
|
|
819
|
+
supervisor,
|
|
820
|
+
PolicyApplier(args.config),
|
|
821
|
+
args.config,
|
|
822
|
+
container.audit_logger,
|
|
823
|
+
)
|
|
824
|
+
poller = None
|
|
825
|
+
if _cloud_features_enabled():
|
|
826
|
+
poller = CommandPoller(CloudRuntimeService(), executor)
|
|
827
|
+
poller.start()
|
|
828
|
+
server = LocalApiServer(host, args.api_port, services)
|
|
829
|
+
print("AgentSecure daemon API: http://%s:%s" % (host, args.api_port), flush=True)
|
|
830
|
+
print("AgentSecure daemon gateway: http://%s:%s" % (host, gateway_port), flush=True)
|
|
831
|
+
try:
|
|
832
|
+
server.serve_forever()
|
|
833
|
+
finally:
|
|
834
|
+
if poller:
|
|
835
|
+
poller.stop()
|
|
836
|
+
return 0
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def guard_command(args: argparse.Namespace) -> int:
|
|
840
|
+
runner = GuardedCommandRunner(args.config)
|
|
841
|
+
return runner.run(args.tool, list(args.tool_args))
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def run_api(args: argparse.Namespace) -> int:
|
|
845
|
+
services = ApiServices(args.config, _scanner())
|
|
846
|
+
server = LocalApiServer(args.host, args.port, services)
|
|
847
|
+
print("AgentSecure API listening on http://%s:%s" % (server.host, server.port))
|
|
848
|
+
server.serve_forever()
|
|
849
|
+
return 0
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _apply_read_only_agent_mode(argv: List[str], read_only_workspace: bool) -> List[str]:
|
|
853
|
+
if not read_only_workspace or not argv:
|
|
854
|
+
return argv
|
|
855
|
+
command = os.path.basename(argv[0])
|
|
856
|
+
if command != "codex":
|
|
857
|
+
return argv
|
|
858
|
+
if "--sandbox" in argv or "-s" in argv:
|
|
859
|
+
return argv
|
|
860
|
+
return [argv[0], "--sandbox", "read-only"] + argv[1:]
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def enroll_cloud(args: argparse.Namespace) -> int:
|
|
864
|
+
if not _cloud_features_enabled():
|
|
865
|
+
return _cloud_features_disabled()
|
|
866
|
+
try:
|
|
867
|
+
result = CloudRuntimeService().enroll(
|
|
868
|
+
args.api_base,
|
|
869
|
+
args.token,
|
|
870
|
+
args.project,
|
|
871
|
+
)
|
|
872
|
+
except CloudError as exc:
|
|
873
|
+
sys.stderr.write("agentsecure: %s\n" % exc)
|
|
874
|
+
return 1
|
|
875
|
+
print("AgentSecure Cloud enrolled.")
|
|
876
|
+
print("Device: %s" % result.get("device_id", ""))
|
|
877
|
+
print("Project: %s" % result.get("project_id", ""))
|
|
878
|
+
if result.get("config_profile"):
|
|
879
|
+
print("Config profile: %s" % _profile_label(result.get("config_profile", {})))
|
|
880
|
+
print("Sync interval: %ss" % result.get("sync_interval_seconds", 30))
|
|
881
|
+
return 0
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def handle_cloud(args: argparse.Namespace) -> int:
|
|
885
|
+
if not _cloud_features_enabled():
|
|
886
|
+
return _cloud_features_disabled()
|
|
887
|
+
service = CloudRuntimeService()
|
|
888
|
+
if args.cloud_command == "status":
|
|
889
|
+
print(json.dumps(service.status(), indent=2, sort_keys=True))
|
|
890
|
+
return 0
|
|
891
|
+
if args.cloud_command == "sync":
|
|
892
|
+
try:
|
|
893
|
+
response = service.sync(
|
|
894
|
+
status="manual",
|
|
895
|
+
force=True,
|
|
896
|
+
)
|
|
897
|
+
try:
|
|
898
|
+
_apply_cloud_policy_response(args.config, response)
|
|
899
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
900
|
+
sys.stderr.write("agentsecure: failed to apply cloud policy: %s\n" % exc)
|
|
901
|
+
return 1
|
|
902
|
+
print(json.dumps(response, indent=2, sort_keys=True))
|
|
903
|
+
except CloudError as exc:
|
|
904
|
+
sys.stderr.write("agentsecure: %s\n" % exc)
|
|
905
|
+
return 1
|
|
906
|
+
return 0
|
|
907
|
+
sys.stderr.write("agentsecure: missing cloud subcommand\n")
|
|
908
|
+
return 2
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
if __name__ == "__main__":
|
|
912
|
+
raise SystemExit(main())
|