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.
Files changed (78) hide show
  1. agentsecure/__init__.py +4 -0
  2. agentsecure/__main__.py +5 -0
  3. agentsecure/api/__init__.py +1 -0
  4. agentsecure/api/server.py +8 -0
  5. agentsecure/api/services.py +3 -0
  6. agentsecure/cli/__init__.py +2 -0
  7. agentsecure/cli/common.py +116 -0
  8. agentsecure/cli/demo.py +97 -0
  9. agentsecure/cli/main.py +912 -0
  10. agentsecure/cli/policy.py +47 -0
  11. agentsecure/cli/project.py +137 -0
  12. agentsecure/cli/secrets.py +293 -0
  13. agentsecure/cli/settings.py +112 -0
  14. agentsecure/client/__init__.py +1 -0
  15. agentsecure/client/wrappers.py +87 -0
  16. agentsecure/cloud.py +30 -0
  17. agentsecure/core/__init__.py +2 -0
  18. agentsecure/core/capabilities.py +109 -0
  19. agentsecure/core/command_metadata.py +13 -0
  20. agentsecure/core/config.py +173 -0
  21. agentsecure/core/config_profiles.py +192 -0
  22. agentsecure/core/container.py +75 -0
  23. agentsecure/core/key_service.py +140 -0
  24. agentsecure/core/models.py +183 -0
  25. agentsecure/core/policy_mutation.py +127 -0
  26. agentsecure/core/policy_ports.py +61 -0
  27. agentsecure/core/policy_response.py +68 -0
  28. agentsecure/core/policy_validation.py +97 -0
  29. agentsecure/core/product.py +267 -0
  30. agentsecure/core/time.py +38 -0
  31. agentsecure/crypto/__init__.py +2 -0
  32. agentsecure/crypto/cipher.py +57 -0
  33. agentsecure/crypto/key_provider.py +39 -0
  34. agentsecure/daemon/__init__.py +1 -0
  35. agentsecure/daemon/commands.py +8 -0
  36. agentsecure/daemon/policies.py +3 -0
  37. agentsecure/daemon/sessions.py +3 -0
  38. agentsecure/daemon/supervisor.py +3 -0
  39. agentsecure/discovery/__init__.py +2 -0
  40. agentsecure/discovery/dotenv_scanner.py +60 -0
  41. agentsecure/discovery/env_scanner.py +29 -0
  42. agentsecure/discovery/patterns.py +90 -0
  43. agentsecure/discovery/scanner.py +27 -0
  44. agentsecure/discovery/suggestions.py +154 -0
  45. agentsecure/gateway/__init__.py +2 -0
  46. agentsecure/gateway/proxy.py +272 -0
  47. agentsecure/guard/__init__.py +2 -0
  48. agentsecure/guard/command.py +62 -0
  49. agentsecure/guard/network.py +92 -0
  50. agentsecure/guard/sanitizer.py +105 -0
  51. agentsecure/guard/wrappers.py +50 -0
  52. agentsecure/implementations/__init__.py +2 -0
  53. agentsecure/implementations/audit.py +53 -0
  54. agentsecure/implementations/encrypted_secret_store.py +73 -0
  55. agentsecure/implementations/grant_store.py +84 -0
  56. agentsecure/implementations/local_secret_store.py +53 -0
  57. agentsecure/implementations/policy.py +126 -0
  58. agentsecure/implementations/secret_store_factory.py +21 -0
  59. agentsecure/implementations/secrets.py +138 -0
  60. agentsecure/interfaces/__init__.py +2 -0
  61. agentsecure/interfaces/audit.py +11 -0
  62. agentsecure/interfaces/grants.py +23 -0
  63. agentsecure/interfaces/key_store.py +15 -0
  64. agentsecure/interfaces/policy.py +24 -0
  65. agentsecure/interfaces/secrets.py +19 -0
  66. agentsecure/workspace/__init__.py +2 -0
  67. agentsecure/workspace/apply.py +141 -0
  68. agentsecure/workspace/diff.py +104 -0
  69. agentsecure/workspace/materializer.py +112 -0
  70. agentsecure/workspace/rewriter.py +54 -0
  71. agentsecure/workspace/strategies.py +140 -0
  72. agentsecure-0.1.0.dist-info/METADATA +181 -0
  73. agentsecure-0.1.0.dist-info/RECORD +78 -0
  74. agentsecure-0.1.0.dist-info/WHEEL +5 -0
  75. agentsecure-0.1.0.dist-info/entry_points.txt +2 -0
  76. agentsecure-0.1.0.dist-info/licenses/LICENSE +68 -0
  77. agentsecure-0.1.0.dist-info/licenses/NOTICE +4 -0
  78. agentsecure-0.1.0.dist-info/top_level.txt +1 -0
@@ -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())