agentsecure 0.1.7__tar.gz → 0.1.9__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 (121) hide show
  1. {agentsecure-0.1.7/agentsecure.egg-info → agentsecure-0.1.9}/PKG-INFO +35 -1
  2. {agentsecure-0.1.7 → agentsecure-0.1.9}/README.md +34 -0
  3. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/__init__.py +1 -1
  4. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/main.py +300 -1
  5. agentsecure-0.1.9/agentsecure/core/agent_guidance.py +69 -0
  6. {agentsecure-0.1.7 → agentsecure-0.1.9/agentsecure.egg-info}/PKG-INFO +35 -1
  7. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure.egg-info/SOURCES.txt +2 -0
  8. {agentsecure-0.1.7 → agentsecure-0.1.9}/setup.cfg +1 -1
  9. agentsecure-0.1.9/tests/test_agent_guidance.py +54 -0
  10. agentsecure-0.1.9/tests/test_secret_aliases.py +232 -0
  11. agentsecure-0.1.7/tests/test_secret_aliases.py +0 -95
  12. {agentsecure-0.1.7 → agentsecure-0.1.9}/LICENSE +0 -0
  13. {agentsecure-0.1.7 → agentsecure-0.1.9}/NOTICE +0 -0
  14. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/__main__.py +0 -0
  15. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/api/__init__.py +0 -0
  16. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/api/server.py +0 -0
  17. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/api/services.py +0 -0
  18. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/__init__.py +0 -0
  19. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/common.py +0 -0
  20. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/demo.py +0 -0
  21. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/policy.py +0 -0
  22. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/project.py +0 -0
  23. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/proxy.py +0 -0
  24. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/receipts.py +0 -0
  25. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/secrets.py +0 -0
  26. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cli/settings.py +0 -0
  27. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/client/__init__.py +0 -0
  28. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/client/wrappers.py +0 -0
  29. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/cloud.py +0 -0
  30. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/config/__init__.py +0 -0
  31. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/config/default_agentsecure.json +0 -0
  32. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/__init__.py +0 -0
  33. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/agentsecure_md.py +0 -0
  34. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/capabilities.py +0 -0
  35. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/command_metadata.py +0 -0
  36. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/config.py +0 -0
  37. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/config_profiles.py +0 -0
  38. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/container.py +0 -0
  39. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/key_service.py +0 -0
  40. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/models.py +0 -0
  41. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/policy_mutation.py +0 -0
  42. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/policy_ports.py +0 -0
  43. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/policy_response.py +0 -0
  44. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/policy_validation.py +0 -0
  45. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/product.py +0 -0
  46. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/provider_proxy.py +0 -0
  47. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/runtime_bindings.py +0 -0
  48. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/secret_aliases.py +0 -0
  49. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/core/time.py +0 -0
  50. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/crypto/__init__.py +0 -0
  51. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/crypto/cipher.py +0 -0
  52. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/crypto/key_provider.py +0 -0
  53. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/daemon/__init__.py +0 -0
  54. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/daemon/commands.py +0 -0
  55. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/daemon/policies.py +0 -0
  56. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/daemon/sessions.py +0 -0
  57. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/daemon/supervisor.py +0 -0
  58. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/discovery/__init__.py +0 -0
  59. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/discovery/dotenv_scanner.py +0 -0
  60. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/discovery/env_scanner.py +0 -0
  61. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/discovery/patterns.py +0 -0
  62. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/discovery/scanner.py +0 -0
  63. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/discovery/suggestions.py +0 -0
  64. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/gateway/__init__.py +0 -0
  65. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/gateway/proxy.py +0 -0
  66. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/guard/__init__.py +0 -0
  67. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/guard/command.py +0 -0
  68. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/guard/network.py +0 -0
  69. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/guard/sanitizer.py +0 -0
  70. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/guard/wrappers.py +0 -0
  71. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/__init__.py +0 -0
  72. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/audit.py +0 -0
  73. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/encrypted_secret_store.py +0 -0
  74. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/grant_store.py +0 -0
  75. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/local_secret_store.py +0 -0
  76. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/policy.py +0 -0
  77. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/secret_store_factory.py +0 -0
  78. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/implementations/secrets.py +0 -0
  79. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/interfaces/__init__.py +0 -0
  80. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/interfaces/audit.py +0 -0
  81. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/interfaces/grants.py +0 -0
  82. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/interfaces/key_store.py +0 -0
  83. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/interfaces/policy.py +0 -0
  84. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/interfaces/secrets.py +0 -0
  85. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/workspace/__init__.py +0 -0
  86. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/workspace/apply.py +0 -0
  87. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/workspace/diff.py +0 -0
  88. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/workspace/materializer.py +0 -0
  89. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/workspace/rewriter.py +0 -0
  90. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure/workspace/strategies.py +0 -0
  91. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure.egg-info/dependency_links.txt +0 -0
  92. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure.egg-info/entry_points.txt +0 -0
  93. {agentsecure-0.1.7 → agentsecure-0.1.9}/agentsecure.egg-info/top_level.txt +0 -0
  94. {agentsecure-0.1.7 → agentsecure-0.1.9}/pyproject.toml +0 -0
  95. {agentsecure-0.1.7 → agentsecure-0.1.9}/setup.py +0 -0
  96. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_agentsecure_md.py +0 -0
  97. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_audit.py +0 -0
  98. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_cli.py +0 -0
  99. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_cli_demo.py +0 -0
  100. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_cli_policy.py +0 -0
  101. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_cli_proxy.py +0 -0
  102. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_config_validation.py +0 -0
  103. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_diff.py +0 -0
  104. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_discovery.py +0 -0
  105. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_encrypted_secret_store.py +0 -0
  106. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_env_policy.py +0 -0
  107. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_env_policy_contract.py +0 -0
  108. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_gateway_credentials.py +0 -0
  109. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_guard.py +0 -0
  110. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_guard_network.py +0 -0
  111. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_key_service.py +0 -0
  112. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_policy.py +0 -0
  113. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_policy_mutation.py +0 -0
  114. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_product.py +0 -0
  115. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_provider_proxy.py +0 -0
  116. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_secret_scan.py +0 -0
  117. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_secrets.py +0 -0
  118. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_version.py +0 -0
  119. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_workspace.py +0 -0
  120. {agentsecure-0.1.7 → agentsecure-0.1.9}/tests/test_workspace_apply.py +0 -0
  121. {agentsecure-0.1.7 → agentsecure-0.1.9}/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.9
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,23 @@ 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
+ To undo the rewrite and bring the original `.env` back from the latest private backup:
89
+
90
+ ```bash
91
+ agentsecure secrets restore .env
92
+ ```
93
+
94
+ For manual control, add one alias at a time:
95
+
79
96
  ```bash
80
97
  printf '%s' "$DATABASE_URL" | agentsecure secrets add dev_db \
81
98
  --env-name DATABASE_URL \
@@ -97,6 +114,23 @@ What this does:
97
114
 
98
115
  Do not put real secrets in project `.env` files. Use `.env` for non-secret config or fake placeholders that are safe for an agent to read.
99
116
 
117
+ ## Agent Run Guidance
118
+
119
+ Every `agentsecure run` creates a small per-run guide under `.agentsecure/runs/` and prints its relative path:
120
+
121
+ ```text
122
+ AgentSecure agent guide: .agentsecure/runs/run_.../AGENTSECURE_AGENT_GUIDE.md
123
+ ```
124
+
125
+ The launched agent receives the absolute guide path in both `AGENTSECURE_AGENT_GUIDE` and `AGENTSECURE_SKILL_FILE`. The file contains only operational guidance and safe metadata, such as managed secret environment variable names, providers, and approved hosts from runtime alias bindings when available. It does not include raw secrets or virtual token values.
126
+
127
+ Agents should use the injected environment variables and virtual tokens. They should not read `.env` to recover secrets or ask a human to paste secrets. If an expected secret env var is missing, ask the user to run:
128
+
129
+ ```bash
130
+ agentsecure secrets import .env
131
+ agentsecure secrets use <alias>
132
+ ```
133
+
100
134
  ## What The Demo Shows
101
135
 
102
136
  The built-in demo creates a temporary local project with fake secrets, applies a small sample policy, simulates a command reading `.env`, and prints what the agent would see:
@@ -50,6 +50,23 @@ 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
+ To undo the rewrite and bring the original `.env` back from the latest private backup:
63
+
64
+ ```bash
65
+ agentsecure secrets restore .env
66
+ ```
67
+
68
+ For manual control, add one alias at a time:
69
+
53
70
  ```bash
54
71
  printf '%s' "$DATABASE_URL" | agentsecure secrets add dev_db \
55
72
  --env-name DATABASE_URL \
@@ -71,6 +88,23 @@ What this does:
71
88
 
72
89
  Do not put real secrets in project `.env` files. Use `.env` for non-secret config or fake placeholders that are safe for an agent to read.
73
90
 
91
+ ## Agent Run Guidance
92
+
93
+ Every `agentsecure run` creates a small per-run guide under `.agentsecure/runs/` and prints its relative path:
94
+
95
+ ```text
96
+ AgentSecure agent guide: .agentsecure/runs/run_.../AGENTSECURE_AGENT_GUIDE.md
97
+ ```
98
+
99
+ The launched agent receives the absolute guide path in both `AGENTSECURE_AGENT_GUIDE` and `AGENTSECURE_SKILL_FILE`. The file contains only operational guidance and safe metadata, such as managed secret environment variable names, providers, and approved hosts from runtime alias bindings when available. It does not include raw secrets or virtual token values.
100
+
101
+ Agents should use the injected environment variables and virtual tokens. They should not read `.env` to recover secrets or ask a human to paste secrets. If an expected secret env var is missing, ask the user to run:
102
+
103
+ ```bash
104
+ agentsecure secrets import .env
105
+ agentsecure secrets use <alias>
106
+ ```
107
+
74
108
  ## What The Demo Shows
75
109
 
76
110
  The built-in demo creates a temporary local project with fake secrets, applies a small sample policy, simulates a command reading `.env`, and prints what the agent would see:
@@ -1,3 +1,3 @@
1
1
  """AgentSecure local-first security runtime."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.9"
@@ -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__
@@ -54,6 +56,12 @@ from agentsecure.cli.settings import (
54
56
  )
55
57
  from agentsecure.client.wrappers import AgentWrapperInstaller, SUPPORTED_AGENTS
56
58
  from agentsecure.core.agentsecure_md import AGENTSECURE_MD, agentsecure_md_status
59
+ from agentsecure.core.agent_guidance import (
60
+ ENV_AGENT_GUIDE,
61
+ ENV_SKILL_FILE,
62
+ relative_agent_guidance_path,
63
+ write_agent_guidance,
64
+ )
57
65
  from agentsecure.core.command_metadata import safe_command_metadata
58
66
  from agentsecure.core.config import ConfigError, JsonConfigLoader, JsonConfigWriter
59
67
  from agentsecure.core.config_profiles import (
@@ -256,6 +264,17 @@ def build_parser() -> argparse.ArgumentParser:
256
264
  use_secret_parser = secrets_subparsers.add_parser("use", help="Assign aliases to this project")
257
265
  use_secret_parser.add_argument("alias_ids", nargs="+")
258
266
  use_secret_parser.add_argument("--project", default="", help="Project name for audit metadata")
267
+ import_secret_parser = secrets_subparsers.add_parser("import", help="Move secrets from a dotenv file into the local vault")
268
+ import_secret_parser.add_argument("path", nargs="?", default=".env", help="Dotenv file to import, default .env")
269
+ import_secret_parser.add_argument("--project", default="", help="Project name for audit metadata")
270
+ import_secret_parser.add_argument("--approved-host", action="append", default=[], help="Additional host allowed to receive imported secrets")
271
+ import_secret_parser.add_argument("--keep-file", action="store_true", help="Do not rewrite the dotenv file after importing")
272
+ import_secret_parser.add_argument("--no-backup", action="store_true", help="Do not store a local backup before rewriting")
273
+ import_secret_parser.add_argument("--dry-run", action="store_true", help="Show what would be imported without writing changes")
274
+ restore_secret_parser = secrets_subparsers.add_parser("restore", help="Restore a dotenv file from the latest AgentSecure backup")
275
+ restore_secret_parser.add_argument("path", nargs="?", default=".env", help="Dotenv file to restore, default .env")
276
+ restore_secret_parser.add_argument("--backup", default="", help="Specific backup file to restore instead of the latest backup")
277
+ restore_secret_parser.add_argument("--dry-run", action="store_true", help="Show which backup would be restored")
259
278
 
260
279
  subparsers.add_parser("discover", help="Discover likely local secrets")
261
280
  subparsers.add_parser("suggest", help="Suggest env and network policy for discovered secrets")
@@ -367,7 +386,8 @@ def run_agent(args: argparse.Namespace) -> int:
367
386
  runtime_bindings = alias_runtime_bindings
368
387
  container = Container.from_config_path(args.config, runtime_bindings=runtime_bindings, run_id=run_id)
369
388
  replacements.extend(_configured_secret_replacements(container, replacements))
370
- run_cwd = os.getcwd()
389
+ source_root = os.getcwd()
390
+ run_cwd = source_root
371
391
  workspace_session = None
372
392
  materializer = WorkspaceMaterializer(_workspace_base_for_runtime(run_cwd, args.runtime))
373
393
  should_create_workspace = bool(replacements or container.config.files.protect_write)
@@ -453,6 +473,24 @@ def run_agent(args: argparse.Namespace) -> int:
453
473
  env[ENV_RUNTIME_BINDINGS] = serialize_runtime_bindings(runtime_bindings)
454
474
  env["AGENTSECURE_RUN_ID"] = run_id
455
475
  env["AGENTSECURE_PROJECT_ID"] = project_id
476
+ try:
477
+ agent_guide_path = write_agent_guidance(source_root, run_id, runtime_bindings)
478
+ except OSError as exc:
479
+ sys.stderr.write("agentsecure: failed to create agent guidance file: %s\n" % exc)
480
+ return 1
481
+ env[ENV_SKILL_FILE] = agent_guide_path
482
+ env[ENV_AGENT_GUIDE] = agent_guide_path
483
+ relative_guide_path = relative_agent_guidance_path(source_root, agent_guide_path)
484
+ container.audit_logger.record(
485
+ "agent_guidance_created",
486
+ {
487
+ "run_id": run_id,
488
+ "project_id": project_id,
489
+ "path": relative_guide_path,
490
+ "aliases": [binding.alias_id or binding.env_name for binding in runtime_bindings],
491
+ },
492
+ )
493
+ print("AgentSecure agent guide: %s" % relative_guide_path, flush=True)
456
494
  if args.runtime == "command-guard":
457
495
  CommandGuardWrapperInstaller(args.config).install(env)
458
496
  session_id = daemon_session.get("session_id", "") if daemon_session else ""
@@ -1149,6 +1187,10 @@ def handle_secret_aliases(args: argparse.Namespace) -> int:
1149
1187
  return list_secret_aliases(args)
1150
1188
  if args.secrets_command == "use":
1151
1189
  return use_secret_aliases(args)
1190
+ if args.secrets_command == "import":
1191
+ return import_secret_aliases(args)
1192
+ if args.secrets_command == "restore":
1193
+ return restore_dotenv_from_backup(args)
1152
1194
  sys.stderr.write("agentsecure: missing secrets subcommand\n")
1153
1195
  return 2
1154
1196
 
@@ -1242,6 +1284,263 @@ def use_secret_aliases(args: argparse.Namespace) -> int:
1242
1284
  return 0
1243
1285
 
1244
1286
 
1287
+ def import_secret_aliases(args: argparse.Namespace) -> int:
1288
+ dotenv_path = os.path.abspath(args.path)
1289
+ if not os.path.exists(dotenv_path):
1290
+ sys.stderr.write("agentsecure: dotenv file not found: %s\n" % args.path)
1291
+ return 2
1292
+ source_dir = os.path.dirname(dotenv_path) or "."
1293
+ source_name = os.path.basename(dotenv_path)
1294
+ discoveries = DotenvSecretScanner(source_dir, [source_name]).scan()
1295
+ discoveries = [secret for secret in discoveries if secret.source == source_name]
1296
+ if not discoveries:
1297
+ print(
1298
+ json.dumps(
1299
+ {
1300
+ "imported": [],
1301
+ "dotenv": args.path,
1302
+ "message": "No secrets found",
1303
+ },
1304
+ indent=2,
1305
+ sort_keys=True,
1306
+ )
1307
+ )
1308
+ return 0
1309
+
1310
+ imports = []
1311
+ for secret in discoveries:
1312
+ alias_id = _alias_id_for_env_name(secret.name)
1313
+ approved_hosts = _approved_hosts_for_import(secret, args.approved_host)
1314
+ imports.append(
1315
+ {
1316
+ "alias_id": alias_id,
1317
+ "env_name": secret.name,
1318
+ "provider": secret.provider_hint,
1319
+ "approved_hosts": approved_hosts,
1320
+ "placeholder": _alias_placeholder(alias_id),
1321
+ }
1322
+ )
1323
+
1324
+ if args.dry_run:
1325
+ print(
1326
+ json.dumps(
1327
+ {
1328
+ "dotenv": args.path,
1329
+ "imported": imports,
1330
+ "would_rewrite_dotenv": not args.keep_file,
1331
+ "dry_run": True,
1332
+ },
1333
+ indent=2,
1334
+ sort_keys=True,
1335
+ )
1336
+ )
1337
+ return 0
1338
+
1339
+ service = _secret_alias_service(args.config)
1340
+ try:
1341
+ for secret, item in zip(discoveries, imports):
1342
+ service.add_alias(
1343
+ alias_id=item["alias_id"],
1344
+ real_secret=secret.value,
1345
+ env_name=secret.name,
1346
+ provider=secret.provider_hint,
1347
+ approved_hosts=item["approved_hosts"],
1348
+ name=secret.name,
1349
+ )
1350
+ assigned = service.assign_to_project(
1351
+ args.config,
1352
+ [item["alias_id"] for item in imports],
1353
+ project=args.project or os.path.basename(os.getcwd()) or "default",
1354
+ )
1355
+ except SecretAliasError as exc:
1356
+ sys.stderr.write("agentsecure: %s\n" % exc)
1357
+ return 2
1358
+
1359
+ backup_path = ""
1360
+ if not args.keep_file:
1361
+ if not args.no_backup:
1362
+ try:
1363
+ backup_path = _backup_dotenv_to_vault(dotenv_path, args.config)
1364
+ except OSError as exc:
1365
+ sys.stderr.write("agentsecure: failed to back up dotenv file: %s\n" % exc)
1366
+ return 1
1367
+ try:
1368
+ _rewrite_dotenv_with_alias_placeholders(dotenv_path, {item["env_name"]: item["placeholder"] for item in imports})
1369
+ except OSError as exc:
1370
+ sys.stderr.write("agentsecure: failed to rewrite dotenv file: %s\n" % exc)
1371
+ return 1
1372
+
1373
+ print(
1374
+ json.dumps(
1375
+ {
1376
+ "dotenv": args.path,
1377
+ "backup": backup_path,
1378
+ "rewritten": not args.keep_file,
1379
+ "imported": [
1380
+ {
1381
+ "alias_id": item.alias_id,
1382
+ "env_name": item.env_name,
1383
+ "provider": item.provider,
1384
+ "approved_hosts": item.approved_hosts,
1385
+ }
1386
+ for item in assigned
1387
+ ],
1388
+ "real_secrets_stored": "local_vault",
1389
+ },
1390
+ indent=2,
1391
+ sort_keys=True,
1392
+ )
1393
+ )
1394
+ return 0
1395
+
1396
+
1397
+ def restore_dotenv_from_backup(args: argparse.Namespace) -> int:
1398
+ dotenv_path = os.path.abspath(args.path)
1399
+ backup_path = os.path.abspath(args.backup) if args.backup else _latest_dotenv_backup(dotenv_path, args.config)
1400
+ if not backup_path:
1401
+ sys.stderr.write("agentsecure: no backup found for %s\n" % args.path)
1402
+ return 2
1403
+ if not os.path.exists(backup_path):
1404
+ sys.stderr.write("agentsecure: backup file not found: %s\n" % backup_path)
1405
+ return 2
1406
+ if args.dry_run:
1407
+ print(
1408
+ json.dumps(
1409
+ {
1410
+ "dotenv": args.path,
1411
+ "backup": backup_path,
1412
+ "would_restore": True,
1413
+ "dry_run": True,
1414
+ },
1415
+ indent=2,
1416
+ sort_keys=True,
1417
+ )
1418
+ )
1419
+ return 0
1420
+ try:
1421
+ shutil.copy2(backup_path, dotenv_path)
1422
+ except OSError as exc:
1423
+ sys.stderr.write("agentsecure: failed to restore dotenv file: %s\n" % exc)
1424
+ return 1
1425
+ print(
1426
+ json.dumps(
1427
+ {
1428
+ "dotenv": args.path,
1429
+ "backup": backup_path,
1430
+ "restored": True,
1431
+ },
1432
+ indent=2,
1433
+ sort_keys=True,
1434
+ )
1435
+ )
1436
+ return 0
1437
+
1438
+
1439
+ def _alias_id_for_env_name(env_name: str) -> str:
1440
+ normalized = re.sub(r"[^a-z0-9._-]+", "_", str(env_name).strip().lower())
1441
+ normalized = normalized.strip("._-")
1442
+ return normalized or "secret"
1443
+
1444
+
1445
+ def _alias_placeholder(alias_id: str) -> str:
1446
+ return "AGENTSECURE_ALIAS_%s" % re.sub(r"[^A-Z0-9]+", "_", alias_id.upper()).strip("_")
1447
+
1448
+
1449
+ def _approved_hosts_for_import(secret: DiscoveredSecret, extra_hosts: List[str]) -> List[str]:
1450
+ hosts = []
1451
+ inferred = _host_from_secret_value(secret.value)
1452
+ if inferred:
1453
+ hosts.append(inferred)
1454
+ provider_hosts = {
1455
+ "openai": "api.openai.com",
1456
+ "anthropic": "api.anthropic.com",
1457
+ "github": "api.github.com",
1458
+ "stripe": "api.stripe.com",
1459
+ }
1460
+ provider_host = provider_hosts.get(secret.provider_hint)
1461
+ if provider_host:
1462
+ hosts.append(provider_host)
1463
+ hosts.extend(extra_hosts or [])
1464
+ result = []
1465
+ seen = set()
1466
+ for host in hosts:
1467
+ normalized = str(host).strip().lower().rstrip(".")
1468
+ if normalized and normalized not in seen:
1469
+ result.append(normalized)
1470
+ seen.add(normalized)
1471
+ return result
1472
+
1473
+
1474
+ def _host_from_secret_value(value: str) -> str:
1475
+ try:
1476
+ parsed = urlsplit(str(value).strip())
1477
+ except ValueError:
1478
+ return ""
1479
+ return (parsed.hostname or "").strip().lower().rstrip(".")
1480
+
1481
+
1482
+ def _backup_dotenv_to_vault(dotenv_path: str, config_path: str) -> str:
1483
+ backup_dir = os.path.join(agentsecure_home(), "backups", project_id_for_path(config_path))
1484
+ os.makedirs(backup_dir, exist_ok=True)
1485
+ backup_path = os.path.join(
1486
+ backup_dir,
1487
+ "%s.%s.bak" % (os.path.basename(dotenv_path), time.strftime("%Y%m%d%H%M%S")),
1488
+ )
1489
+ shutil.copy2(dotenv_path, backup_path)
1490
+ os.chmod(backup_path, 0o600)
1491
+ return backup_path
1492
+
1493
+
1494
+ def _latest_dotenv_backup(dotenv_path: str, config_path: str) -> str:
1495
+ backup_dir = os.path.join(agentsecure_home(), "backups", project_id_for_path(config_path))
1496
+ if not os.path.isdir(backup_dir):
1497
+ return ""
1498
+ prefix = os.path.basename(dotenv_path) + "."
1499
+ candidates = []
1500
+ for filename in os.listdir(backup_dir):
1501
+ if filename.startswith(prefix) and filename.endswith(".bak"):
1502
+ path = os.path.join(backup_dir, filename)
1503
+ if os.path.isfile(path):
1504
+ candidates.append(path)
1505
+ if not candidates:
1506
+ return ""
1507
+ candidates.sort(key=lambda path: (os.path.getmtime(path), path), reverse=True)
1508
+ return candidates[0]
1509
+
1510
+
1511
+ def _rewrite_dotenv_with_alias_placeholders(dotenv_path: str, placeholders: Dict[str, str]) -> None:
1512
+ with open(dotenv_path, "r") as handle:
1513
+ lines = handle.readlines()
1514
+ rewritten = []
1515
+ for line in lines:
1516
+ parsed_name = _dotenv_line_name(line)
1517
+ if parsed_name and parsed_name in placeholders:
1518
+ prefix = line.split("=", 1)[0].rstrip()
1519
+ newline = "\n" if line.endswith("\n") else ""
1520
+ rewritten.append("%s=%s%s" % (prefix, placeholders[parsed_name], newline))
1521
+ else:
1522
+ rewritten.append(line)
1523
+ fd, temp_path = tempfile.mkstemp(prefix=".agentsecure-dotenv-", dir=os.path.dirname(dotenv_path) or ".")
1524
+ try:
1525
+ with os.fdopen(fd, "w") as handle:
1526
+ handle.writelines(rewritten)
1527
+ shutil.copymode(dotenv_path, temp_path)
1528
+ os.replace(temp_path, dotenv_path)
1529
+ finally:
1530
+ if os.path.exists(temp_path):
1531
+ os.unlink(temp_path)
1532
+
1533
+
1534
+ def _dotenv_line_name(line: str) -> str:
1535
+ stripped = line.strip()
1536
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
1537
+ return ""
1538
+ name = stripped.split("=", 1)[0].strip()
1539
+ if name.startswith("export "):
1540
+ name = name[len("export ") :].strip()
1541
+ return name
1542
+
1543
+
1245
1544
  def _secret_alias_service(config_path: str) -> SecretAliasService:
1246
1545
  home = agentsecure_home()
1247
1546
  return SecretAliasService(
@@ -0,0 +1,69 @@
1
+ import os
2
+ from typing import Iterable, List
3
+
4
+ from agentsecure.core.models import SecretBinding
5
+
6
+
7
+ AGENT_GUIDE_FILENAME = "AGENTSECURE_AGENT_GUIDE.md"
8
+ ENV_AGENT_GUIDE = "AGENTSECURE_AGENT_GUIDE"
9
+ ENV_SKILL_FILE = "AGENTSECURE_SKILL_FILE"
10
+
11
+
12
+ def write_agent_guidance(source_root: str, run_id: str, bindings: Iterable[SecretBinding]) -> str:
13
+ guide_dir = os.path.abspath(os.path.join(source_root, ".agentsecure", "runs", run_id))
14
+ os.makedirs(guide_dir, exist_ok=True)
15
+ os.chmod(guide_dir, 0o700)
16
+ guide_path = os.path.join(guide_dir, AGENT_GUIDE_FILENAME)
17
+ fd = os.open(guide_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
18
+ with os.fdopen(fd, "w") as handle:
19
+ handle.write(render_agent_guidance(bindings))
20
+ os.chmod(guide_path, 0o600)
21
+ return guide_path
22
+
23
+
24
+ def render_agent_guidance(bindings: Iterable[SecretBinding]) -> str:
25
+ binding_list = sorted(list(bindings), key=lambda item: (item.env_name, item.provider, item.alias_id))
26
+ lines: List[str] = [
27
+ "# AgentSecure Run Guidance",
28
+ "",
29
+ "- Use the environment variables AgentSecure injected for this run.",
30
+ "- Treat values beginning with `virt_` as virtual tokens, not raw secrets.",
31
+ "- Do not read `.env` files to recover secrets.",
32
+ "- Do not ask a human to paste secrets into chat, terminal, logs, or files.",
33
+ "- If a required secret environment variable is missing, ask the user to run `agentsecure secrets import .env` or `agentsecure secrets use <alias>` before retrying.",
34
+ "",
35
+ ]
36
+ if binding_list:
37
+ lines.extend(["## Managed Secret Environment", ""])
38
+ for binding in binding_list:
39
+ details = [
40
+ "env=%s" % _safe_text(binding.env_name),
41
+ "provider=%s" % _safe_text(binding.provider or "custom"),
42
+ ]
43
+ if binding.approved_hosts:
44
+ details.append("approved_hosts=%s" % ", ".join(_safe_text(host) for host in binding.approved_hosts))
45
+ else:
46
+ details.append("approved_hosts=not specified")
47
+ lines.append("- " + "; ".join(details))
48
+ lines.append("")
49
+ else:
50
+ lines.extend(
51
+ [
52
+ "## Managed Secret Environment",
53
+ "",
54
+ "- No runtime alias bindings were provided for this run.",
55
+ "",
56
+ ]
57
+ )
58
+ return "\n".join(lines)
59
+
60
+
61
+ def relative_agent_guidance_path(source_root: str, guide_path: str) -> str:
62
+ try:
63
+ return os.path.relpath(guide_path, os.path.abspath(source_root))
64
+ except ValueError:
65
+ return guide_path
66
+
67
+
68
+ def _safe_text(value: str) -> str:
69
+ return str(value).replace("\n", " ").replace("\r", " ").strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentsecure
3
- Version: 0.1.7
3
+ Version: 0.1.9
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,23 @@ 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
+ To undo the rewrite and bring the original `.env` back from the latest private backup:
89
+
90
+ ```bash
91
+ agentsecure secrets restore .env
92
+ ```
93
+
94
+ For manual control, add one alias at a time:
95
+
79
96
  ```bash
80
97
  printf '%s' "$DATABASE_URL" | agentsecure secrets add dev_db \
81
98
  --env-name DATABASE_URL \
@@ -97,6 +114,23 @@ What this does:
97
114
 
98
115
  Do not put real secrets in project `.env` files. Use `.env` for non-secret config or fake placeholders that are safe for an agent to read.
99
116
 
117
+ ## Agent Run Guidance
118
+
119
+ Every `agentsecure run` creates a small per-run guide under `.agentsecure/runs/` and prints its relative path:
120
+
121
+ ```text
122
+ AgentSecure agent guide: .agentsecure/runs/run_.../AGENTSECURE_AGENT_GUIDE.md
123
+ ```
124
+
125
+ The launched agent receives the absolute guide path in both `AGENTSECURE_AGENT_GUIDE` and `AGENTSECURE_SKILL_FILE`. The file contains only operational guidance and safe metadata, such as managed secret environment variable names, providers, and approved hosts from runtime alias bindings when available. It does not include raw secrets or virtual token values.
126
+
127
+ Agents should use the injected environment variables and virtual tokens. They should not read `.env` to recover secrets or ask a human to paste secrets. If an expected secret env var is missing, ask the user to run:
128
+
129
+ ```bash
130
+ agentsecure secrets import .env
131
+ agentsecure secrets use <alias>
132
+ ```
133
+
100
134
  ## What The Demo Shows
101
135
 
102
136
  The built-in demo creates a temporary local project with fake secrets, applies a small sample policy, simulates a command reading `.env`, and prints what the agent would see:
@@ -30,6 +30,7 @@ agentsecure/client/wrappers.py
30
30
  agentsecure/config/__init__.py
31
31
  agentsecure/config/default_agentsecure.json
32
32
  agentsecure/core/__init__.py
33
+ agentsecure/core/agent_guidance.py
33
34
  agentsecure/core/agentsecure_md.py
34
35
  agentsecure/core/capabilities.py
35
36
  agentsecure/core/command_metadata.py
@@ -88,6 +89,7 @@ agentsecure/workspace/diff.py
88
89
  agentsecure/workspace/materializer.py
89
90
  agentsecure/workspace/rewriter.py
90
91
  agentsecure/workspace/strategies.py
92
+ tests/test_agent_guidance.py
91
93
  tests/test_agentsecure_md.py
92
94
  tests/test_audit.py
93
95
  tests/test_cli.py
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = agentsecure
3
- version = 0.1.7
3
+ version = 0.1.9
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,54 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+
5
+ from agentsecure.core.agent_guidance import (
6
+ AGENT_GUIDE_FILENAME,
7
+ render_agent_guidance,
8
+ relative_agent_guidance_path,
9
+ write_agent_guidance,
10
+ )
11
+ from agentsecure.core.models import SecretBinding
12
+
13
+
14
+ class AgentGuidanceTest(unittest.TestCase):
15
+ def test_renders_managed_bindings_without_virtual_or_real_secret_values(self):
16
+ text = render_agent_guidance(
17
+ [
18
+ SecretBinding(
19
+ env_name="OPENAI_API_KEY",
20
+ virtual_token="virt_openai_should_not_be_written",
21
+ real_secret_ref="vault://secret-ref-should-not-be-written",
22
+ provider="openai",
23
+ alias_id="openai_dev",
24
+ approved_hosts=["api.openai.com"],
25
+ )
26
+ ]
27
+ )
28
+
29
+ self.assertIn("OPENAI_API_KEY", text)
30
+ self.assertIn("provider=openai", text)
31
+ self.assertIn("approved_hosts=api.openai.com", text)
32
+ self.assertIn("Do not read `.env` files", text)
33
+ self.assertIn("agentsecure secrets import .env", text)
34
+ self.assertIn("agentsecure secrets use <alias>", text)
35
+ self.assertNotIn("virt_openai_should_not_be_written", text)
36
+ self.assertNotIn("vault://secret-ref-should-not-be-written", text)
37
+
38
+ def test_writes_per_run_guidance_under_agentsecure_state(self):
39
+ with tempfile.TemporaryDirectory() as temp_dir:
40
+ path = write_agent_guidance(temp_dir, "run_test", [])
41
+
42
+ self.assertEqual(
43
+ os.path.join(temp_dir, ".agentsecure", "runs", "run_test", AGENT_GUIDE_FILENAME),
44
+ path,
45
+ )
46
+ self.assertTrue(os.path.exists(path))
47
+ self.assertEqual(
48
+ os.path.join(".agentsecure", "runs", "run_test", AGENT_GUIDE_FILENAME),
49
+ relative_agent_guidance_path(temp_dir, path),
50
+ )
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()