agentsecure 0.1.7__tar.gz → 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {agentsecure-0.1.7/agentsecure.egg-info → agentsecure-0.1.8}/PKG-INFO +12 -1
  2. {agentsecure-0.1.7 → agentsecure-0.1.8}/README.md +11 -0
  3. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/__init__.py +1 -1
  4. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/main.py +209 -0
  5. {agentsecure-0.1.7 → agentsecure-0.1.8/agentsecure.egg-info}/PKG-INFO +12 -1
  6. {agentsecure-0.1.7 → agentsecure-0.1.8}/setup.cfg +1 -1
  7. agentsecure-0.1.8/tests/test_secret_aliases.py +186 -0
  8. agentsecure-0.1.7/tests/test_secret_aliases.py +0 -95
  9. {agentsecure-0.1.7 → agentsecure-0.1.8}/LICENSE +0 -0
  10. {agentsecure-0.1.7 → agentsecure-0.1.8}/NOTICE +0 -0
  11. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/__main__.py +0 -0
  12. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/api/__init__.py +0 -0
  13. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/api/server.py +0 -0
  14. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/api/services.py +0 -0
  15. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/__init__.py +0 -0
  16. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/common.py +0 -0
  17. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/demo.py +0 -0
  18. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/policy.py +0 -0
  19. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/project.py +0 -0
  20. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/proxy.py +0 -0
  21. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/receipts.py +0 -0
  22. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/secrets.py +0 -0
  23. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cli/settings.py +0 -0
  24. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/client/__init__.py +0 -0
  25. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/client/wrappers.py +0 -0
  26. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/cloud.py +0 -0
  27. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/config/__init__.py +0 -0
  28. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/config/default_agentsecure.json +0 -0
  29. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/__init__.py +0 -0
  30. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/agentsecure_md.py +0 -0
  31. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/capabilities.py +0 -0
  32. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/command_metadata.py +0 -0
  33. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/config.py +0 -0
  34. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/config_profiles.py +0 -0
  35. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/container.py +0 -0
  36. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/key_service.py +0 -0
  37. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/models.py +0 -0
  38. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/policy_mutation.py +0 -0
  39. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/policy_ports.py +0 -0
  40. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/policy_response.py +0 -0
  41. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/policy_validation.py +0 -0
  42. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/product.py +0 -0
  43. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/provider_proxy.py +0 -0
  44. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/runtime_bindings.py +0 -0
  45. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/secret_aliases.py +0 -0
  46. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/core/time.py +0 -0
  47. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/crypto/__init__.py +0 -0
  48. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/crypto/cipher.py +0 -0
  49. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/crypto/key_provider.py +0 -0
  50. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/daemon/__init__.py +0 -0
  51. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/daemon/commands.py +0 -0
  52. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/daemon/policies.py +0 -0
  53. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/daemon/sessions.py +0 -0
  54. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/daemon/supervisor.py +0 -0
  55. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/discovery/__init__.py +0 -0
  56. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/discovery/dotenv_scanner.py +0 -0
  57. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/discovery/env_scanner.py +0 -0
  58. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/discovery/patterns.py +0 -0
  59. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/discovery/scanner.py +0 -0
  60. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/discovery/suggestions.py +0 -0
  61. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/gateway/__init__.py +0 -0
  62. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/gateway/proxy.py +0 -0
  63. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/guard/__init__.py +0 -0
  64. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/guard/command.py +0 -0
  65. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/guard/network.py +0 -0
  66. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/guard/sanitizer.py +0 -0
  67. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/guard/wrappers.py +0 -0
  68. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/__init__.py +0 -0
  69. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/audit.py +0 -0
  70. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/encrypted_secret_store.py +0 -0
  71. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/grant_store.py +0 -0
  72. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/local_secret_store.py +0 -0
  73. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/policy.py +0 -0
  74. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/secret_store_factory.py +0 -0
  75. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/implementations/secrets.py +0 -0
  76. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/interfaces/__init__.py +0 -0
  77. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/interfaces/audit.py +0 -0
  78. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/interfaces/grants.py +0 -0
  79. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/interfaces/key_store.py +0 -0
  80. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/interfaces/policy.py +0 -0
  81. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/interfaces/secrets.py +0 -0
  82. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/workspace/__init__.py +0 -0
  83. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/workspace/apply.py +0 -0
  84. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/workspace/diff.py +0 -0
  85. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/workspace/materializer.py +0 -0
  86. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/workspace/rewriter.py +0 -0
  87. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure/workspace/strategies.py +0 -0
  88. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure.egg-info/SOURCES.txt +0 -0
  89. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure.egg-info/dependency_links.txt +0 -0
  90. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure.egg-info/entry_points.txt +0 -0
  91. {agentsecure-0.1.7 → agentsecure-0.1.8}/agentsecure.egg-info/top_level.txt +0 -0
  92. {agentsecure-0.1.7 → agentsecure-0.1.8}/pyproject.toml +0 -0
  93. {agentsecure-0.1.7 → agentsecure-0.1.8}/setup.py +0 -0
  94. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_agentsecure_md.py +0 -0
  95. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_audit.py +0 -0
  96. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_cli.py +0 -0
  97. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_cli_demo.py +0 -0
  98. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_cli_policy.py +0 -0
  99. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_cli_proxy.py +0 -0
  100. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_config_validation.py +0 -0
  101. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_diff.py +0 -0
  102. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_discovery.py +0 -0
  103. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_encrypted_secret_store.py +0 -0
  104. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_env_policy.py +0 -0
  105. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_env_policy_contract.py +0 -0
  106. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_gateway_credentials.py +0 -0
  107. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_guard.py +0 -0
  108. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_guard_network.py +0 -0
  109. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_key_service.py +0 -0
  110. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_policy.py +0 -0
  111. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_policy_mutation.py +0 -0
  112. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_product.py +0 -0
  113. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_provider_proxy.py +0 -0
  114. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_secret_scan.py +0 -0
  115. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_secrets.py +0 -0
  116. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_version.py +0 -0
  117. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_workspace.py +0 -0
  118. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_workspace_apply.py +0 -0
  119. {agentsecure-0.1.7 → agentsecure-0.1.8}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentsecure
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -76,6 +76,17 @@ python3 -m agentsecure run claude
76
76
 
77
77
  Keep real secrets in one local AgentSecure vault:
78
78
 
79
+ ```bash
80
+ agentsecure secrets import .env
81
+ agentsecure run -- claude
82
+ ```
83
+
84
+ `secrets import` is the easiest migration path. It scans the dotenv file, stores discovered real secret values in the local vault, assigns those aliases to the current project, writes a private backup under `~/.agentsecure/backups/`, and replaces the values in `.env` with non-secret `AGENTSECURE_ALIAS_...` placeholders.
85
+
86
+ Use `--dry-run` to preview the import, or `--keep-file` if you want to store aliases without rewriting `.env`.
87
+
88
+ For manual control, add one alias at a time:
89
+
79
90
  ```bash
80
91
  printf '%s' "$DATABASE_URL" | agentsecure secrets add dev_db \
81
92
  --env-name DATABASE_URL \
@@ -50,6 +50,17 @@ python3 -m agentsecure run claude
50
50
 
51
51
  Keep real secrets in one local AgentSecure vault:
52
52
 
53
+ ```bash
54
+ agentsecure secrets import .env
55
+ agentsecure run -- claude
56
+ ```
57
+
58
+ `secrets import` is the easiest migration path. It scans the dotenv file, stores discovered real secret values in the local vault, assigns those aliases to the current project, writes a private backup under `~/.agentsecure/backups/`, and replaces the values in `.env` with non-secret `AGENTSECURE_ALIAS_...` placeholders.
59
+
60
+ Use `--dry-run` to preview the import, or `--keep-file` if you want to store aliases without rewriting `.env`.
61
+
62
+ For manual control, add one alias at a time:
63
+
53
64
  ```bash
54
65
  printf '%s' "$DATABASE_URL" | agentsecure secrets add dev_db \
55
66
  --env-name DATABASE_URL \
@@ -1,3 +1,3 @@
1
1
  """AgentSecure local-first security runtime."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.8"
@@ -5,6 +5,7 @@ import ipaddress
5
5
  import json
6
6
  import os
7
7
  import queue
8
+ import re
8
9
  import shutil
9
10
  import socket
10
11
  import subprocess
@@ -14,6 +15,7 @@ import threading
14
15
  import time
15
16
  import uuid
16
17
  from typing import Any, Dict, List, Optional
18
+ from urllib.parse import urlsplit
17
19
  from urllib.request import Request, urlopen
18
20
 
19
21
  from agentsecure import __version__
@@ -256,6 +258,13 @@ def build_parser() -> argparse.ArgumentParser:
256
258
  use_secret_parser = secrets_subparsers.add_parser("use", help="Assign aliases to this project")
257
259
  use_secret_parser.add_argument("alias_ids", nargs="+")
258
260
  use_secret_parser.add_argument("--project", default="", help="Project name for audit metadata")
261
+ import_secret_parser = secrets_subparsers.add_parser("import", help="Move secrets from a dotenv file into the local vault")
262
+ import_secret_parser.add_argument("path", nargs="?", default=".env", help="Dotenv file to import, default .env")
263
+ import_secret_parser.add_argument("--project", default="", help="Project name for audit metadata")
264
+ import_secret_parser.add_argument("--approved-host", action="append", default=[], help="Additional host allowed to receive imported secrets")
265
+ import_secret_parser.add_argument("--keep-file", action="store_true", help="Do not rewrite the dotenv file after importing")
266
+ import_secret_parser.add_argument("--no-backup", action="store_true", help="Do not store a local backup before rewriting")
267
+ import_secret_parser.add_argument("--dry-run", action="store_true", help="Show what would be imported without writing changes")
259
268
 
260
269
  subparsers.add_parser("discover", help="Discover likely local secrets")
261
270
  subparsers.add_parser("suggest", help="Suggest env and network policy for discovered secrets")
@@ -1149,6 +1158,8 @@ def handle_secret_aliases(args: argparse.Namespace) -> int:
1149
1158
  return list_secret_aliases(args)
1150
1159
  if args.secrets_command == "use":
1151
1160
  return use_secret_aliases(args)
1161
+ if args.secrets_command == "import":
1162
+ return import_secret_aliases(args)
1152
1163
  sys.stderr.write("agentsecure: missing secrets subcommand\n")
1153
1164
  return 2
1154
1165
 
@@ -1242,6 +1253,204 @@ def use_secret_aliases(args: argparse.Namespace) -> int:
1242
1253
  return 0
1243
1254
 
1244
1255
 
1256
+ def import_secret_aliases(args: argparse.Namespace) -> int:
1257
+ dotenv_path = os.path.abspath(args.path)
1258
+ if not os.path.exists(dotenv_path):
1259
+ sys.stderr.write("agentsecure: dotenv file not found: %s\n" % args.path)
1260
+ return 2
1261
+ source_dir = os.path.dirname(dotenv_path) or "."
1262
+ source_name = os.path.basename(dotenv_path)
1263
+ discoveries = DotenvSecretScanner(source_dir, [source_name]).scan()
1264
+ discoveries = [secret for secret in discoveries if secret.source == source_name]
1265
+ if not discoveries:
1266
+ print(
1267
+ json.dumps(
1268
+ {
1269
+ "imported": [],
1270
+ "dotenv": args.path,
1271
+ "message": "No secrets found",
1272
+ },
1273
+ indent=2,
1274
+ sort_keys=True,
1275
+ )
1276
+ )
1277
+ return 0
1278
+
1279
+ imports = []
1280
+ for secret in discoveries:
1281
+ alias_id = _alias_id_for_env_name(secret.name)
1282
+ approved_hosts = _approved_hosts_for_import(secret, args.approved_host)
1283
+ imports.append(
1284
+ {
1285
+ "alias_id": alias_id,
1286
+ "env_name": secret.name,
1287
+ "provider": secret.provider_hint,
1288
+ "approved_hosts": approved_hosts,
1289
+ "placeholder": _alias_placeholder(alias_id),
1290
+ }
1291
+ )
1292
+
1293
+ if args.dry_run:
1294
+ print(
1295
+ json.dumps(
1296
+ {
1297
+ "dotenv": args.path,
1298
+ "imported": imports,
1299
+ "would_rewrite_dotenv": not args.keep_file,
1300
+ "dry_run": True,
1301
+ },
1302
+ indent=2,
1303
+ sort_keys=True,
1304
+ )
1305
+ )
1306
+ return 0
1307
+
1308
+ service = _secret_alias_service(args.config)
1309
+ try:
1310
+ for secret, item in zip(discoveries, imports):
1311
+ service.add_alias(
1312
+ alias_id=item["alias_id"],
1313
+ real_secret=secret.value,
1314
+ env_name=secret.name,
1315
+ provider=secret.provider_hint,
1316
+ approved_hosts=item["approved_hosts"],
1317
+ name=secret.name,
1318
+ )
1319
+ assigned = service.assign_to_project(
1320
+ args.config,
1321
+ [item["alias_id"] for item in imports],
1322
+ project=args.project or os.path.basename(os.getcwd()) or "default",
1323
+ )
1324
+ except SecretAliasError as exc:
1325
+ sys.stderr.write("agentsecure: %s\n" % exc)
1326
+ return 2
1327
+
1328
+ backup_path = ""
1329
+ if not args.keep_file:
1330
+ if not args.no_backup:
1331
+ try:
1332
+ backup_path = _backup_dotenv_to_vault(dotenv_path, args.config)
1333
+ except OSError as exc:
1334
+ sys.stderr.write("agentsecure: failed to back up dotenv file: %s\n" % exc)
1335
+ return 1
1336
+ try:
1337
+ _rewrite_dotenv_with_alias_placeholders(dotenv_path, {item["env_name"]: item["placeholder"] for item in imports})
1338
+ except OSError as exc:
1339
+ sys.stderr.write("agentsecure: failed to rewrite dotenv file: %s\n" % exc)
1340
+ return 1
1341
+
1342
+ print(
1343
+ json.dumps(
1344
+ {
1345
+ "dotenv": args.path,
1346
+ "backup": backup_path,
1347
+ "rewritten": not args.keep_file,
1348
+ "imported": [
1349
+ {
1350
+ "alias_id": item.alias_id,
1351
+ "env_name": item.env_name,
1352
+ "provider": item.provider,
1353
+ "approved_hosts": item.approved_hosts,
1354
+ }
1355
+ for item in assigned
1356
+ ],
1357
+ "real_secrets_stored": "local_vault",
1358
+ },
1359
+ indent=2,
1360
+ sort_keys=True,
1361
+ )
1362
+ )
1363
+ return 0
1364
+
1365
+
1366
+ def _alias_id_for_env_name(env_name: str) -> str:
1367
+ normalized = re.sub(r"[^a-z0-9._-]+", "_", str(env_name).strip().lower())
1368
+ normalized = normalized.strip("._-")
1369
+ return normalized or "secret"
1370
+
1371
+
1372
+ def _alias_placeholder(alias_id: str) -> str:
1373
+ return "AGENTSECURE_ALIAS_%s" % re.sub(r"[^A-Z0-9]+", "_", alias_id.upper()).strip("_")
1374
+
1375
+
1376
+ def _approved_hosts_for_import(secret: DiscoveredSecret, extra_hosts: List[str]) -> List[str]:
1377
+ hosts = []
1378
+ inferred = _host_from_secret_value(secret.value)
1379
+ if inferred:
1380
+ hosts.append(inferred)
1381
+ provider_hosts = {
1382
+ "openai": "api.openai.com",
1383
+ "anthropic": "api.anthropic.com",
1384
+ "github": "api.github.com",
1385
+ "stripe": "api.stripe.com",
1386
+ }
1387
+ provider_host = provider_hosts.get(secret.provider_hint)
1388
+ if provider_host:
1389
+ hosts.append(provider_host)
1390
+ hosts.extend(extra_hosts or [])
1391
+ result = []
1392
+ seen = set()
1393
+ for host in hosts:
1394
+ normalized = str(host).strip().lower().rstrip(".")
1395
+ if normalized and normalized not in seen:
1396
+ result.append(normalized)
1397
+ seen.add(normalized)
1398
+ return result
1399
+
1400
+
1401
+ def _host_from_secret_value(value: str) -> str:
1402
+ try:
1403
+ parsed = urlsplit(str(value).strip())
1404
+ except ValueError:
1405
+ return ""
1406
+ return (parsed.hostname or "").strip().lower().rstrip(".")
1407
+
1408
+
1409
+ def _backup_dotenv_to_vault(dotenv_path: str, config_path: str) -> str:
1410
+ backup_dir = os.path.join(agentsecure_home(), "backups", project_id_for_path(config_path))
1411
+ os.makedirs(backup_dir, exist_ok=True)
1412
+ backup_path = os.path.join(
1413
+ backup_dir,
1414
+ "%s.%s.bak" % (os.path.basename(dotenv_path), time.strftime("%Y%m%d%H%M%S")),
1415
+ )
1416
+ shutil.copy2(dotenv_path, backup_path)
1417
+ os.chmod(backup_path, 0o600)
1418
+ return backup_path
1419
+
1420
+
1421
+ def _rewrite_dotenv_with_alias_placeholders(dotenv_path: str, placeholders: Dict[str, str]) -> None:
1422
+ with open(dotenv_path, "r") as handle:
1423
+ lines = handle.readlines()
1424
+ rewritten = []
1425
+ for line in lines:
1426
+ parsed_name = _dotenv_line_name(line)
1427
+ if parsed_name and parsed_name in placeholders:
1428
+ prefix = line.split("=", 1)[0].rstrip()
1429
+ newline = "\n" if line.endswith("\n") else ""
1430
+ rewritten.append("%s=%s%s" % (prefix, placeholders[parsed_name], newline))
1431
+ else:
1432
+ rewritten.append(line)
1433
+ fd, temp_path = tempfile.mkstemp(prefix=".agentsecure-dotenv-", dir=os.path.dirname(dotenv_path) or ".")
1434
+ try:
1435
+ with os.fdopen(fd, "w") as handle:
1436
+ handle.writelines(rewritten)
1437
+ shutil.copymode(dotenv_path, temp_path)
1438
+ os.replace(temp_path, dotenv_path)
1439
+ finally:
1440
+ if os.path.exists(temp_path):
1441
+ os.unlink(temp_path)
1442
+
1443
+
1444
+ def _dotenv_line_name(line: str) -> str:
1445
+ stripped = line.strip()
1446
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
1447
+ return ""
1448
+ name = stripped.split("=", 1)[0].strip()
1449
+ if name.startswith("export "):
1450
+ name = name[len("export ") :].strip()
1451
+ return name
1452
+
1453
+
1245
1454
  def _secret_alias_service(config_path: str) -> SecretAliasService:
1246
1455
  home = agentsecure_home()
1247
1456
  return SecretAliasService(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentsecure
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -76,6 +76,17 @@ python3 -m agentsecure run claude
76
76
 
77
77
  Keep real secrets in one local AgentSecure vault:
78
78
 
79
+ ```bash
80
+ agentsecure secrets import .env
81
+ agentsecure run -- claude
82
+ ```
83
+
84
+ `secrets import` is the easiest migration path. It scans the dotenv file, stores discovered real secret values in the local vault, assigns those aliases to the current project, writes a private backup under `~/.agentsecure/backups/`, and replaces the values in `.env` with non-secret `AGENTSECURE_ALIAS_...` placeholders.
85
+
86
+ Use `--dry-run` to preview the import, or `--keep-file` if you want to store aliases without rewriting `.env`.
87
+
88
+ For manual control, add one alias at a time:
89
+
79
90
  ```bash
80
91
  printf '%s' "$DATABASE_URL" | agentsecure secrets add dev_db \
81
92
  --env-name DATABASE_URL \
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = agentsecure
3
- version = 0.1.7
3
+ version = 0.1.8
4
4
  description = Local-first security runtime for AI coding agents
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -0,0 +1,186 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+ import unittest
5
+ from contextlib import redirect_stdout
6
+ from io import StringIO
7
+
8
+ from agentsecure.cli.main import main
9
+ from agentsecure.core.config import JsonConfigLoader
10
+ from agentsecure.core.container import Container
11
+
12
+
13
+ class SecretAliasesCliTest(unittest.TestCase):
14
+ def test_import_dotenv_moves_secrets_to_vault_and_rewrites_file(self):
15
+ with tempfile.TemporaryDirectory() as temp_dir:
16
+ project_dir = os.path.join(temp_dir, "project")
17
+ home_dir = os.path.join(temp_dir, "home")
18
+ os.makedirs(project_dir)
19
+ config_path = os.path.join(project_dir, "agentsecure.json")
20
+ env_path = os.path.join(project_dir, ".env")
21
+ real_url = "postgres://user:password@dev.example.invalid/mydb"
22
+ real_key = "sk_test_dummy_value_do_not_use"
23
+ with open(env_path, "w") as handle:
24
+ handle.write("DATABASE_URL=%s\n" % real_url)
25
+ handle.write("STRIPE_API_KEY=%s\n" % real_key)
26
+ handle.write("DEBUG=true\n")
27
+
28
+ old_cwd = os.getcwd()
29
+ old_home = os.environ.get("AGENTSECURE_HOME")
30
+ os.environ["AGENTSECURE_HOME"] = home_dir
31
+ try:
32
+ os.chdir(project_dir)
33
+ output = StringIO()
34
+ with redirect_stdout(output):
35
+ self.assertEqual(0, main(["--config", config_path, "secrets", "import", ".env"]))
36
+
37
+ result = json.loads(output.getvalue())
38
+ self.assertTrue(result["rewritten"])
39
+ self.assertEqual("local_vault", result["real_secrets_stored"])
40
+ self.assertTrue(os.path.exists(result["backup"]))
41
+ self.assertTrue(result["backup"].startswith(home_dir))
42
+
43
+ with open(env_path, "r") as handle:
44
+ dotenv_text = handle.read()
45
+ self.assertIn("DATABASE_URL=AGENTSECURE_ALIAS_DATABASE_URL", dotenv_text)
46
+ self.assertIn("STRIPE_API_KEY=AGENTSECURE_ALIAS_STRIPE_API_KEY", dotenv_text)
47
+ self.assertIn("DEBUG=true", dotenv_text)
48
+ self.assertNotIn(real_url, dotenv_text)
49
+ self.assertNotIn(real_key, dotenv_text)
50
+
51
+ with open(config_path, "r") as handle:
52
+ raw = json.load(handle)
53
+ raw_text = json.dumps(raw)
54
+ self.assertNotIn(real_url, raw_text)
55
+ self.assertNotIn(real_key, raw_text)
56
+ self.assertEqual(
57
+ ["database_url", "stripe_api_key"],
58
+ [item["alias_id"] for item in raw["secret_aliases"]],
59
+ )
60
+ self.assertIn("dev.example.invalid", raw["network"]["allow_domains"])
61
+ self.assertIn("api.stripe.com", raw["network"]["allow_domains"])
62
+
63
+ with open(result["backup"], "r") as handle:
64
+ backup_text = handle.read()
65
+ self.assertIn(real_url, backup_text)
66
+ self.assertIn(real_key, backup_text)
67
+ finally:
68
+ os.chdir(old_cwd)
69
+ if old_home is None:
70
+ os.environ.pop("AGENTSECURE_HOME", None)
71
+ else:
72
+ os.environ["AGENTSECURE_HOME"] = old_home
73
+
74
+ def test_import_dotenv_dry_run_does_not_write(self):
75
+ with tempfile.TemporaryDirectory() as temp_dir:
76
+ project_dir = os.path.join(temp_dir, "project")
77
+ home_dir = os.path.join(temp_dir, "home")
78
+ os.makedirs(project_dir)
79
+ config_path = os.path.join(project_dir, "agentsecure.json")
80
+ env_path = os.path.join(project_dir, ".env")
81
+ real_url = "postgres://user:password@dev.example.invalid/mydb"
82
+ with open(env_path, "w") as handle:
83
+ handle.write("DATABASE_URL=%s\n" % real_url)
84
+
85
+ old_cwd = os.getcwd()
86
+ old_home = os.environ.get("AGENTSECURE_HOME")
87
+ os.environ["AGENTSECURE_HOME"] = home_dir
88
+ try:
89
+ os.chdir(project_dir)
90
+ output = StringIO()
91
+ with redirect_stdout(output):
92
+ self.assertEqual(0, main(["--config", config_path, "secrets", "import", ".env", "--dry-run"]))
93
+ result = json.loads(output.getvalue())
94
+ self.assertTrue(result["dry_run"])
95
+ self.assertFalse(os.path.exists(config_path))
96
+ with open(env_path, "r") as handle:
97
+ self.assertIn(real_url, handle.read())
98
+ finally:
99
+ os.chdir(old_cwd)
100
+ if old_home is None:
101
+ os.environ.pop("AGENTSECURE_HOME", None)
102
+ else:
103
+ os.environ["AGENTSECURE_HOME"] = old_home
104
+
105
+ def test_project_assignment_stores_only_alias_metadata(self):
106
+ with tempfile.TemporaryDirectory() as temp_dir:
107
+ project_dir = os.path.join(temp_dir, "project")
108
+ home_dir = os.path.join(temp_dir, "home")
109
+ os.makedirs(project_dir)
110
+ config_path = os.path.join(project_dir, "agentsecure.json")
111
+ old_cwd = os.getcwd()
112
+ old_home = os.environ.get("AGENTSECURE_HOME")
113
+ os.environ["AGENTSECURE_HOME"] = home_dir
114
+ try:
115
+ os.chdir(project_dir)
116
+ os.environ["TEST_REAL_DATABASE_URL"] = "database-secret-value-for-test"
117
+ with redirect_stdout(StringIO()):
118
+ self.assertEqual(
119
+ 0,
120
+ main(
121
+ [
122
+ "--config",
123
+ config_path,
124
+ "secrets",
125
+ "add",
126
+ "dev_db",
127
+ "--env-name",
128
+ "DATABASE_URL",
129
+ "--provider",
130
+ "database",
131
+ "--approved-host",
132
+ "db.example.com",
133
+ "--real-secret-env",
134
+ "TEST_REAL_DATABASE_URL",
135
+ ]
136
+ ),
137
+ )
138
+ self.assertEqual(0, main(["--config", config_path, "secrets", "use", "dev_db"]))
139
+
140
+ with open(config_path, "r") as handle:
141
+ raw = json.load(handle)
142
+ self.assertEqual("dev_db", raw["secret_aliases"][0]["alias_id"])
143
+ self.assertNotIn("database-secret-value-for-test", json.dumps(raw))
144
+ self.assertIn("db.example.com", raw["network"]["allow_domains"])
145
+
146
+ config = JsonConfigLoader().load(config_path)
147
+ from agentsecure.core.secret_aliases import SecretAliasService, local_secret_alias_store_for_home, project_id_for_path
148
+ from agentsecure.implementations.grant_store import local_grant_store_for_config
149
+ from agentsecure.implementations.secret_store_factory import encrypted_secret_store_for_vault
150
+
151
+ class MemoryAudit:
152
+ def record(self, event_type, details):
153
+ pass
154
+
155
+ bindings = SecretAliasService(
156
+ local_secret_alias_store_for_home(home_dir),
157
+ encrypted_secret_store_for_vault(),
158
+ local_grant_store_for_config(config_path),
159
+ MemoryAudit(),
160
+ ).prepare_run_bindings(config.secret_aliases, "2h", project_id_for_path(config_path), "run_1")
161
+ container = Container.from_config_path(config_path, runtime_bindings=bindings, run_id="run_1")
162
+ token = bindings[0].virtual_token
163
+ self.assertEqual(
164
+ "database-secret-value-for-test",
165
+ container.token_resolver.resolve(
166
+ token,
167
+ {"host": "db.example.com", "project_id": container.project_id, "run_id": "run_1"},
168
+ ),
169
+ )
170
+ self.assertIsNone(
171
+ container.token_resolver.resolve(
172
+ token,
173
+ {"host": "evil.example.com", "project_id": container.project_id, "run_id": "run_1"},
174
+ )
175
+ )
176
+ finally:
177
+ os.chdir(old_cwd)
178
+ os.environ.pop("TEST_REAL_DATABASE_URL", None)
179
+ if old_home is None:
180
+ os.environ.pop("AGENTSECURE_HOME", None)
181
+ else:
182
+ os.environ["AGENTSECURE_HOME"] = old_home
183
+
184
+
185
+ if __name__ == "__main__":
186
+ unittest.main()
@@ -1,95 +0,0 @@
1
- import json
2
- import os
3
- import tempfile
4
- import unittest
5
- from contextlib import redirect_stdout
6
- from io import StringIO
7
-
8
- from agentsecure.cli.main import main
9
- from agentsecure.core.config import JsonConfigLoader
10
- from agentsecure.core.container import Container
11
-
12
-
13
- class SecretAliasesCliTest(unittest.TestCase):
14
- def test_project_assignment_stores_only_alias_metadata(self):
15
- with tempfile.TemporaryDirectory() as temp_dir:
16
- project_dir = os.path.join(temp_dir, "project")
17
- home_dir = os.path.join(temp_dir, "home")
18
- os.makedirs(project_dir)
19
- config_path = os.path.join(project_dir, "agentsecure.json")
20
- old_cwd = os.getcwd()
21
- old_home = os.environ.get("AGENTSECURE_HOME")
22
- os.environ["AGENTSECURE_HOME"] = home_dir
23
- try:
24
- os.chdir(project_dir)
25
- os.environ["TEST_REAL_DATABASE_URL"] = "database-secret-value-for-test"
26
- with redirect_stdout(StringIO()):
27
- self.assertEqual(
28
- 0,
29
- main(
30
- [
31
- "--config",
32
- config_path,
33
- "secrets",
34
- "add",
35
- "dev_db",
36
- "--env-name",
37
- "DATABASE_URL",
38
- "--provider",
39
- "database",
40
- "--approved-host",
41
- "db.example.com",
42
- "--real-secret-env",
43
- "TEST_REAL_DATABASE_URL",
44
- ]
45
- ),
46
- )
47
- self.assertEqual(0, main(["--config", config_path, "secrets", "use", "dev_db"]))
48
-
49
- with open(config_path, "r") as handle:
50
- raw = json.load(handle)
51
- self.assertEqual("dev_db", raw["secret_aliases"][0]["alias_id"])
52
- self.assertNotIn("database-secret-value-for-test", json.dumps(raw))
53
- self.assertIn("db.example.com", raw["network"]["allow_domains"])
54
-
55
- config = JsonConfigLoader().load(config_path)
56
- from agentsecure.core.secret_aliases import SecretAliasService, local_secret_alias_store_for_home, project_id_for_path
57
- from agentsecure.implementations.grant_store import local_grant_store_for_config
58
- from agentsecure.implementations.secret_store_factory import encrypted_secret_store_for_vault
59
-
60
- class MemoryAudit:
61
- def record(self, event_type, details):
62
- pass
63
-
64
- bindings = SecretAliasService(
65
- local_secret_alias_store_for_home(home_dir),
66
- encrypted_secret_store_for_vault(),
67
- local_grant_store_for_config(config_path),
68
- MemoryAudit(),
69
- ).prepare_run_bindings(config.secret_aliases, "2h", project_id_for_path(config_path), "run_1")
70
- container = Container.from_config_path(config_path, runtime_bindings=bindings, run_id="run_1")
71
- token = bindings[0].virtual_token
72
- self.assertEqual(
73
- "database-secret-value-for-test",
74
- container.token_resolver.resolve(
75
- token,
76
- {"host": "db.example.com", "project_id": container.project_id, "run_id": "run_1"},
77
- ),
78
- )
79
- self.assertIsNone(
80
- container.token_resolver.resolve(
81
- token,
82
- {"host": "evil.example.com", "project_id": container.project_id, "run_id": "run_1"},
83
- )
84
- )
85
- finally:
86
- os.chdir(old_cwd)
87
- os.environ.pop("TEST_REAL_DATABASE_URL", None)
88
- if old_home is None:
89
- os.environ.pop("AGENTSECURE_HOME", None)
90
- else:
91
- os.environ["AGENTSECURE_HOME"] = old_home
92
-
93
-
94
- if __name__ == "__main__":
95
- unittest.main()
File without changes
File without changes
File without changes
File without changes