skeyd 0.2.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.
skeyd/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ # skeyd — a Zero-Trust secrets broker for AI agents and automation.
2
+ #
3
+ # * skeyd stores credentials encrypted at rest and hands them to child processes under an explicit policy, instead of letting a program read them back. The difference matters for agent workloads: an agent that can *use* a credential without ever *holding* one cannot leak it into a context window, a transcript, or a bug report.
4
+ #
5
+ # Typical library use::
6
+ #
7
+ # from skeyd import AccessRequest, PolicyEngine, load_policy
8
+ #
9
+ # engine = PolicyEngine(load_policy(Path("policy.toml")))
10
+ # decision = engine.evaluate(AccessRequest(label="OPENAI_API_KEY", argv=("python3", "job.py")))
11
+ # if decision.allowed:
12
+ # ...
13
+ #
14
+ # The CLI is the primary interface; see ``skeyd --help`` or the README.
15
+
16
+ from __future__ import annotations
17
+
18
+ __version__ = "0.2.0"
19
+ __all__ = [
20
+ "AccessRequest",
21
+ "AuditEvent",
22
+ "AuditLog",
23
+ "Decision",
24
+ "EncryptedFileStore",
25
+ "ExitCode",
26
+ "Policy",
27
+ "PolicyEngine",
28
+ "Redactor",
29
+ "SecretValue",
30
+ "SkeydError",
31
+ "__version__",
32
+ "load_policy",
33
+ "resolve_paths",
34
+ ]
35
+
36
+
37
+ def __getattr__(name: str) -> object:
38
+ # Resolve public names lazily.
39
+ #
40
+ # Importing ``skeyd`` should not pull in the cryptography backend, the policy parser and the subprocess machinery just so a caller can read ``__version__`` — which is exactly what packaging tools and ``--version`` do most often.
41
+ if name in {"AccessRequest", "Decision", "Policy", "PolicyEngine", "load_policy"}:
42
+ from . import policy
43
+
44
+ return getattr(policy, name)
45
+ if name in {"AuditEvent", "AuditLog"}:
46
+ from . import audit
47
+
48
+ return getattr(audit, name)
49
+ if name == "EncryptedFileStore":
50
+ from .store import EncryptedFileStore
51
+
52
+ return EncryptedFileStore
53
+ if name in {"ExitCode", "SkeydError"}:
54
+ from . import errors
55
+
56
+ return getattr(errors, name)
57
+ if name == "Redactor":
58
+ from .redaction import Redactor
59
+
60
+ return Redactor
61
+ if name == "SecretValue":
62
+ from .secretvalue import SecretValue
63
+
64
+ return SecretValue
65
+ if name == "resolve_paths":
66
+ from .paths import resolve_paths
67
+
68
+ return resolve_paths
69
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
skeyd/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ # Allow ``python -m skeyd`` as an alternative to the console script.
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli.main import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
skeyd/agent.py ADDED
@@ -0,0 +1,248 @@
1
+ # The machine-readable interface for AI agents.
2
+ #
3
+ # An agent integrates with skeyd the same way it integrates with any other tool: it asks for a manifest of callable tools with JSON Schema inputs, then calls them by name. ``skeyd agent manifest`` emits that manifest in whichever dialect the calling framework expects, and ``skeyd agent call`` executes one tool and returns a JSON envelope.
4
+ #
5
+ # * **There is deliberately no ``get_secret`` tool.**
6
+ #
7
+ # That absence is the whole design. A tool that returns plaintext would put the credential into the model's context window, where it is copied into transcripts, evaluation datasets, error reports and — for hosted models — a third party's infrastructure. skeyd's agent surface lets an agent *use* a credential (by running a command with it injected) and *reason about* credentials (names, metadata, what policy permits) without ever holding one.
8
+ #
9
+ # Every tool here delegates to the same functions the CLI uses, so the two surfaces cannot drift apart: an authorization check that applies to ``skeyd run`` applies identically to ``skeyd_run``.
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Final
14
+
15
+ __all__ = [
16
+ "AGENT_SCHEMA_VERSION",
17
+ "TOOLS",
18
+ "ManifestFormat",
19
+ "build_manifest",
20
+ "tool_names",
21
+ ]
22
+
23
+ #: Version of the agent contract: tool names, input schemas and envelope shape.
24
+ #: Bumped only on a breaking change, so an integration can assert at startup.
25
+ AGENT_SCHEMA_VERSION: Final = 1
26
+
27
+ ManifestFormat = str # "native" | "anthropic" | "openai" | "mcp"
28
+
29
+ _LABEL = {
30
+ "type": "string",
31
+ "description": "The label the credential is stored under, e.g. 'OPENAI_API_KEY'.",
32
+ }
33
+
34
+ _COMMAND = {
35
+ "type": "array",
36
+ "items": {"type": "string"},
37
+ "minItems": 1,
38
+ "description": (
39
+ "The command to run, as an argv array (not a shell string). "
40
+ 'Example: ["python3", "jobs/summarise.py", "--input", "data.json"].'
41
+ ),
42
+ }
43
+
44
+ _PURPOSE = {
45
+ "type": "string",
46
+ "description": (
47
+ "Short human-readable reason for this access, recorded in the audit log. "
48
+ "Some policies require it."
49
+ ),
50
+ }
51
+
52
+
53
+ TOOLS: Final[tuple[dict[str, Any], ...]] = (
54
+ {
55
+ "name": "skeyd_list_secrets",
56
+ "description": (
57
+ "List the credential labels available in the store, with metadata: how many "
58
+ "values each holds, which environment variable it maps to, whether any have "
59
+ "expired, and whether a policy rule permits using it. Never returns secret "
60
+ "values — skeyd is a broker, not a vault you can read from."
61
+ ),
62
+ "input_schema": {
63
+ "type": "object",
64
+ "properties": {},
65
+ "additionalProperties": False,
66
+ },
67
+ },
68
+ {
69
+ "name": "skeyd_check_access",
70
+ "description": (
71
+ "Ask whether a given command would be permitted to use a given credential, "
72
+ "without running anything and without decrypting the store. Use this before "
73
+ "skeyd_run to find out what you are allowed to do, and to get a precise "
74
+ "explanation when something is forbidden."
75
+ ),
76
+ "input_schema": {
77
+ "type": "object",
78
+ "properties": {
79
+ "label": _LABEL,
80
+ "command": _COMMAND,
81
+ "purpose": _PURPOSE,
82
+ "cwd": {
83
+ "type": "string",
84
+ "description": "Working directory the command would run in.",
85
+ },
86
+ },
87
+ "required": ["label", "command"],
88
+ "additionalProperties": False,
89
+ },
90
+ },
91
+ {
92
+ "name": "skeyd_run",
93
+ "description": (
94
+ "Run a command with one credential injected into its environment. The "
95
+ "credential is never returned to you and never appears on the command line. "
96
+ "Output is scrubbed of the credential before you see it. Returns the exit "
97
+ "code, duration, and (when capture is enabled) the redacted stdout/stderr. "
98
+ "Denied requests return ok=false with a policy explanation."
99
+ ),
100
+ "input_schema": {
101
+ "type": "object",
102
+ "properties": {
103
+ "label": _LABEL,
104
+ "command": _COMMAND,
105
+ "purpose": _PURPOSE,
106
+ "timeout": {
107
+ "type": "integer",
108
+ "minimum": 1,
109
+ "description": (
110
+ "Wall-clock limit in seconds. If this exceeds the ceiling the "
111
+ "matching policy rule allows, the request is denied outright "
112
+ "(not silently capped) — omit this to use the rule's own ceiling."
113
+ ),
114
+ },
115
+ "cwd": {
116
+ "type": "string",
117
+ "description": "Working directory for the command.",
118
+ },
119
+ "capture_output": {
120
+ "type": "boolean",
121
+ "description": (
122
+ "Return the command's redacted stdout/stderr in the response. "
123
+ "Defaults to the policy setting."
124
+ ),
125
+ },
126
+ },
127
+ "required": ["label", "command"],
128
+ "additionalProperties": False,
129
+ },
130
+ },
131
+ {
132
+ "name": "skeyd_describe_policy",
133
+ "description": (
134
+ "Describe the rules that govern credential use — optionally narrowed to one "
135
+ "label. Use this to understand the boundaries you are operating within before "
136
+ "proposing a plan that policy would reject."
137
+ ),
138
+ "input_schema": {
139
+ "type": "object",
140
+ "properties": {
141
+ "label": {
142
+ "type": "string",
143
+ "description": "Restrict the output to rules matching this label.",
144
+ }
145
+ },
146
+ "additionalProperties": False,
147
+ },
148
+ },
149
+ {
150
+ "name": "skeyd_suggest_label",
151
+ "description": (
152
+ "Propose a conventional label and environment variable name for a new "
153
+ "credential, given the URL or product it came from. Use this when asking a "
154
+ "human to add a key, so the name matches existing conventions."
155
+ ),
156
+ "input_schema": {
157
+ "type": "object",
158
+ "properties": {
159
+ "context": {
160
+ "type": "string",
161
+ "description": (
162
+ "A URL or product description, e.g. "
163
+ "'https://platform.openai.com/api-keys' or 'OpenRouter'."
164
+ ),
165
+ },
166
+ "limit": {
167
+ "type": "integer",
168
+ "minimum": 1,
169
+ "maximum": 10,
170
+ "description": "How many suggestions to return (default 3).",
171
+ },
172
+ },
173
+ "required": ["context"],
174
+ "additionalProperties": False,
175
+ },
176
+ },
177
+ )
178
+
179
+
180
+ def tool_names() -> tuple[str, ...]:
181
+ return tuple(str(tool["name"]) for tool in TOOLS)
182
+
183
+
184
+ def _as_anthropic(tool: dict[str, Any]) -> dict[str, Any]:
185
+ return {
186
+ "name": tool["name"],
187
+ "description": tool["description"],
188
+ "input_schema": tool["input_schema"],
189
+ }
190
+
191
+
192
+ def _as_openai(tool: dict[str, Any]) -> dict[str, Any]:
193
+ return {
194
+ "type": "function",
195
+ "function": {
196
+ "name": tool["name"],
197
+ "description": tool["description"],
198
+ "parameters": tool["input_schema"],
199
+ },
200
+ }
201
+
202
+
203
+ def _as_mcp(tool: dict[str, Any]) -> dict[str, Any]:
204
+ return {
205
+ "name": tool["name"],
206
+ "description": tool["description"],
207
+ "inputSchema": tool["input_schema"],
208
+ }
209
+
210
+
211
+ _FORMATTERS = {
212
+ "native": lambda tool: dict(tool),
213
+ "anthropic": _as_anthropic,
214
+ "openai": _as_openai,
215
+ "mcp": _as_mcp,
216
+ }
217
+
218
+ MANIFEST_FORMATS: Final = tuple(_FORMATTERS)
219
+
220
+
221
+ def build_manifest(*, fmt: ManifestFormat = "native", version: str = "0.0.0") -> dict[str, Any]:
222
+ # Return the tool manifest in the requested dialect.
223
+ #
224
+ # The tool *definitions* are identical across formats; only the envelope shape differs, because the major frameworks disagree on where to put the schema.
225
+ formatter = _FORMATTERS.get(fmt)
226
+ if formatter is None:
227
+ from .errors import UsageError
228
+
229
+ raise UsageError(
230
+ f"Unknown manifest format {fmt!r}.",
231
+ hint=f"Choose one of: {', '.join(MANIFEST_FORMATS)}",
232
+ )
233
+
234
+ tools = [formatter(tool) for tool in TOOLS]
235
+ if fmt == "openai":
236
+ return {"tools": tools}
237
+ if fmt == "mcp":
238
+ return {"tools": tools}
239
+ return {
240
+ "agent_schema_version": AGENT_SCHEMA_VERSION,
241
+ "skeyd_version": version,
242
+ "tools": tools,
243
+ "notes": [
244
+ "No tool returns a plaintext credential. Injection happens inside skeyd.",
245
+ "Call skeyd_check_access before skeyd_run to avoid predictable denials.",
246
+ "A denial is a normal outcome, not an error: read error.hint and adapt.",
247
+ ],
248
+ }