agentveil-posture 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.
@@ -0,0 +1,6 @@
1
+ """AgentVeil Posture static scanner."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
6
+
@@ -0,0 +1,98 @@
1
+ """Command-line interface for AgentVeil Posture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+ from typing import Sequence
10
+
11
+ from agentveil_posture.report import SEVERITIES, Finding, PostureReport
12
+ from agentveil_posture.scanner import ScanError, scan_path
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="agentveil",
18
+ description="AgentVeil developer tools.",
19
+ )
20
+ subparsers = parser.add_subparsers(dest="command", required=True)
21
+
22
+ posture_parser = subparsers.add_parser(
23
+ "posture",
24
+ help="Run static posture checks.",
25
+ )
26
+ posture_subparsers = posture_parser.add_subparsers(
27
+ dest="posture_command",
28
+ required=True,
29
+ )
30
+
31
+ scan_parser = posture_subparsers.add_parser(
32
+ "scan",
33
+ help="Scan a project and write a posture report.",
34
+ )
35
+ scan_parser.add_argument(
36
+ "--path",
37
+ default=".",
38
+ help="Project path to scan. Defaults to the current directory.",
39
+ )
40
+ scan_parser.add_argument(
41
+ "--output",
42
+ required=True,
43
+ help="Path where the report will be written.",
44
+ )
45
+ scan_parser.add_argument(
46
+ "--format",
47
+ choices=("json", "sarif"),
48
+ default="json",
49
+ help="Report format to write. Defaults to json.",
50
+ )
51
+ scan_parser.add_argument(
52
+ "--fail-on",
53
+ choices=SEVERITIES,
54
+ default=None,
55
+ help="Exit 1 when findings at or above this severity are present.",
56
+ )
57
+ scan_parser.set_defaults(handler=_handle_posture_scan)
58
+
59
+ return parser
60
+
61
+
62
+ def _finding_meets_threshold(finding: Finding, threshold: str) -> bool:
63
+ return SEVERITIES.index(finding.severity) <= SEVERITIES.index(threshold)
64
+
65
+
66
+ def _report_meets_threshold(report: PostureReport, threshold: str | None) -> bool:
67
+ if threshold is None:
68
+ return False
69
+ return any(_finding_meets_threshold(finding, threshold) for finding in report.findings)
70
+
71
+
72
+ def _handle_posture_scan(args: argparse.Namespace) -> int:
73
+ try:
74
+ report = scan_path(Path(args.path))
75
+ if args.format == "json":
76
+ output = report.to_json()
77
+ else:
78
+ output = json.dumps(report.to_sarif(), indent=2, sort_keys=True) + "\n"
79
+ Path(args.output).write_text(output, encoding="utf-8")
80
+ if _report_meets_threshold(report, args.fail_on):
81
+ return 1
82
+ return 0
83
+ except (OSError, ScanError) as exc:
84
+ print(f"agentveil posture scan: {exc}", file=sys.stderr)
85
+ return 1
86
+
87
+
88
+ def main(argv: Sequence[str] | None = None) -> int:
89
+ parser = build_parser()
90
+ args = parser.parse_args(argv)
91
+ handler = getattr(args, "handler", None)
92
+ if handler is None:
93
+ parser.error("missing command")
94
+ return int(handler(args))
95
+
96
+
97
+ if __name__ == "__main__":
98
+ raise SystemExit(main())
@@ -0,0 +1,210 @@
1
+ """Versioned JSON report model for AgentVeil Posture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from datetime import datetime, timezone
7
+ import hashlib
8
+ import json
9
+
10
+
11
+ REPORT_VERSION = "0.1"
12
+ SCANNER_VERSION = "agentveil-posture/0.1.0"
13
+ SEVERITIES = ("critical", "high", "medium", "low", "info")
14
+ SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
15
+ SARIF_VERSION = "2.1.0"
16
+ SEVERITY_TO_SARIF_LEVEL = {
17
+ "critical": "error",
18
+ "high": "error",
19
+ "medium": "warning",
20
+ "low": "note",
21
+ "info": "note",
22
+ }
23
+ SEVERITY_TO_SECURITY_SEVERITY = {
24
+ "critical": "9.5",
25
+ "high": "8.0",
26
+ "medium": "5.0",
27
+ "low": "3.0",
28
+ "info": "0.0",
29
+ }
30
+ RULE_DESCRIPTORS = {
31
+ "bypass.direct_github_token": {
32
+ "short": "Direct GitHub token capability",
33
+ "full": "Flags direct GitHub token references in workflows or agent manifests.",
34
+ "help": "https://github.com/agentveil-protocol/agentveil-posture#triaging-findings",
35
+ },
36
+ "workflow.deploy_without_approval": {
37
+ "short": "Deployment without approval gate",
38
+ "full": "Flags deploy, release, or publish workflow steps without an approval signal.",
39
+ "help": "https://github.com/agentveil-protocol/agentveil-posture#triaging-findings",
40
+ },
41
+ "workflow.pull_request_target_secrets_risk": {
42
+ "short": "Privileged pull_request_target risk",
43
+ "full": "Flags pull_request_target workflows that combine privileged context with checkout, run, or secrets.",
44
+ "help": "https://github.com/agentveil-protocol/agentveil-posture#triaging-findings",
45
+ },
46
+ "tool.shell_without_approval": {
47
+ "short": "Shell-capable tool without approval",
48
+ "full": "Flags agent tool manifests that expose shell execution without an approval flag.",
49
+ "help": "https://github.com/agentveil-protocol/agentveil-posture#triaging-findings",
50
+ },
51
+ "identity.private_key_unencrypted": {
52
+ "short": "Unencrypted private key file",
53
+ "full": "Flags committed PEM private key files that appear to be unencrypted.",
54
+ "help": "https://github.com/agentveil-protocol/agentveil-posture#triaging-findings",
55
+ },
56
+ }
57
+
58
+
59
+ def utc_now_iso() -> str:
60
+ return (
61
+ datetime.now(timezone.utc)
62
+ .replace(microsecond=0)
63
+ .isoformat()
64
+ .replace("+00:00", "Z")
65
+ )
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class Finding:
70
+ rule_id: str
71
+ severity: str
72
+ file: str
73
+ line: int | None
74
+ message: str
75
+ remediation: str
76
+
77
+ def __post_init__(self) -> None:
78
+ if self.severity not in SEVERITIES:
79
+ raise ValueError(f"unknown severity: {self.severity}")
80
+ category, separator, name = self.rule_id.partition(".")
81
+ if not category or separator != "." or not name:
82
+ raise ValueError(f"invalid rule_id: {self.rule_id}")
83
+ allowed = set("abcdefghijklmnopqrstuvwxyz0123456789_")
84
+ if not set(category) <= allowed or not set(name) <= allowed:
85
+ raise ValueError(f"invalid rule_id: {self.rule_id}")
86
+ if (
87
+ self.file.startswith("/")
88
+ or self.file.startswith("\\\\")
89
+ or (len(self.file) >= 2 and self.file[1] == ":")
90
+ ):
91
+ raise ValueError("finding file must be repository-relative")
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class Summary:
96
+ by_severity: dict[str, int]
97
+ total: int
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class PostureReport:
102
+ report_version: str
103
+ scanner_version: str
104
+ scanned_at: str
105
+ scanned_path: str
106
+ findings: list[Finding] = field(default_factory=list)
107
+ summary: Summary = field(default_factory=lambda: empty_summary())
108
+
109
+ def to_json(self) -> str:
110
+ return json.dumps(asdict(self), indent=2, sort_keys=True) + "\n"
111
+
112
+ def to_sarif(self) -> dict[str, object]:
113
+ rule_ids = sorted(RULE_DESCRIPTORS)
114
+ rule_index = {rule_id: index for index, rule_id in enumerate(rule_ids)}
115
+ return {
116
+ "$schema": SARIF_SCHEMA,
117
+ "version": SARIF_VERSION,
118
+ "runs": [
119
+ {
120
+ "tool": {
121
+ "driver": {
122
+ "name": "AgentVeil Posture",
123
+ "informationUri": "https://github.com/agentveil-protocol/agentveil-posture",
124
+ "semanticVersion": "0.1.0",
125
+ "rules": [_sarif_rule(rule_id) for rule_id in rule_ids],
126
+ }
127
+ },
128
+ "results": [
129
+ _sarif_result(finding, rule_index[finding.rule_id])
130
+ for finding in self.findings
131
+ ],
132
+ }
133
+ ],
134
+ }
135
+
136
+
137
+ def empty_summary() -> Summary:
138
+ return Summary(by_severity={severity: 0 for severity in SEVERITIES}, total=0)
139
+
140
+
141
+ def empty_report(scanned_path: str) -> PostureReport:
142
+ return build_report(scanned_path, [])
143
+
144
+
145
+ def build_report(scanned_path: str, findings: list[Finding]) -> PostureReport:
146
+ by_severity = {severity: 0 for severity in SEVERITIES}
147
+ for finding in findings:
148
+ by_severity[finding.severity] += 1
149
+ return PostureReport(
150
+ report_version=REPORT_VERSION,
151
+ scanner_version=SCANNER_VERSION,
152
+ scanned_at=utc_now_iso(),
153
+ scanned_path=scanned_path,
154
+ findings=findings,
155
+ summary=Summary(by_severity=by_severity, total=len(findings)),
156
+ )
157
+
158
+
159
+ def _sarif_rule(rule_id: str) -> dict[str, object]:
160
+ descriptor = RULE_DESCRIPTORS[rule_id]
161
+ return {
162
+ "id": rule_id,
163
+ "shortDescription": {"text": descriptor["short"]},
164
+ "fullDescription": {"text": descriptor["full"]},
165
+ "helpUri": descriptor["help"],
166
+ "defaultConfiguration": {
167
+ "level": SEVERITY_TO_SARIF_LEVEL["high"],
168
+ },
169
+ "properties": {
170
+ "security-severity": SEVERITY_TO_SECURITY_SEVERITY["high"],
171
+ },
172
+ }
173
+
174
+
175
+ def _sarif_result(finding: Finding, rule_index: int) -> dict[str, object]:
176
+ physical_location: dict[str, object] = {
177
+ "artifactLocation": {"uri": finding.file.replace("\\", "/")}
178
+ }
179
+ if finding.line is not None:
180
+ physical_location["region"] = {"startLine": finding.line}
181
+
182
+ return {
183
+ "ruleId": finding.rule_id,
184
+ "ruleIndex": rule_index,
185
+ "level": SEVERITY_TO_SARIF_LEVEL[finding.severity],
186
+ "message": {"text": finding.message},
187
+ "locations": [
188
+ {
189
+ "physicalLocation": physical_location,
190
+ }
191
+ ],
192
+ "partialFingerprints": {
193
+ "primaryLocationLineHash": _sarif_fingerprint(finding),
194
+ },
195
+ "properties": {
196
+ "severity": finding.severity,
197
+ "remediation": finding.remediation,
198
+ },
199
+ }
200
+
201
+
202
+ def _sarif_fingerprint(finding: Finding) -> str:
203
+ source = "\0".join(
204
+ [
205
+ finding.rule_id,
206
+ finding.file.replace("\\", "/"),
207
+ "" if finding.line is None else str(finding.line),
208
+ ]
209
+ )
210
+ return hashlib.sha256(source.encode("utf-8")).hexdigest()
@@ -0,0 +1,21 @@
1
+ """Rule registry stubs for AgentVeil Posture v0.1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agentveil_posture.rules.identity import scan_identity_private_key_unencrypted
6
+ from agentveil_posture.rules.manifest import is_agent_manifest, scan_manifest_rules
7
+ from agentveil_posture.rules.workflow import scan_workflow_rules
8
+
9
+ RULES = (
10
+ scan_identity_private_key_unencrypted,
11
+ scan_workflow_rules,
12
+ scan_manifest_rules,
13
+ )
14
+
15
+ __all__ = [
16
+ "RULES",
17
+ "is_agent_manifest",
18
+ "scan_identity_private_key_unencrypted",
19
+ "scan_manifest_rules",
20
+ "scan_workflow_rules",
21
+ ]
@@ -0,0 +1,51 @@
1
+ """Identity-related posture rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from agentveil_posture.report import Finding
8
+
9
+
10
+ HEADER_LIMIT_BYTES = 4096
11
+ PRIVATE_KEY_HEADERS = (
12
+ b"-----BEGIN PRIVATE KEY-----",
13
+ b"-----BEGIN RSA PRIVATE KEY-----",
14
+ b"-----BEGIN EC PRIVATE KEY-----",
15
+ b"-----BEGIN OPENSSH PRIVATE KEY-----",
16
+ )
17
+ ENCRYPTED_KEY_MARKERS = (
18
+ b"-----BEGIN ENCRYPTED PRIVATE KEY-----",
19
+ b"Proc-Type: 4,ENCRYPTED",
20
+ )
21
+
22
+
23
+ def scan_identity_private_key_unencrypted(root: Path, path: Path) -> Finding | None:
24
+ """Detect unencrypted private-key headers without exposing key material.
25
+
26
+ Legacy PEM encryption markers are expected in the bounded header window,
27
+ matching standard PEM output.
28
+ """
29
+ try:
30
+ with path.open("rb") as handle:
31
+ header = handle.read(HEADER_LIMIT_BYTES)
32
+ except OSError:
33
+ return None
34
+
35
+ if any(marker in header for marker in ENCRYPTED_KEY_MARKERS):
36
+ return None
37
+ if not any(header.startswith(marker) for marker in PRIVATE_KEY_HEADERS):
38
+ return None
39
+
40
+ return Finding(
41
+ rule_id="identity.private_key_unencrypted",
42
+ severity="high",
43
+ file=path.relative_to(root).as_posix(),
44
+ line=1,
45
+ message="Unencrypted private key file appears present.",
46
+ remediation=(
47
+ "Remove the key from the repository, rotate it if exposed, store it "
48
+ "in a secret manager, and use encrypted private key material when "
49
+ "local keys are unavoidable."
50
+ ),
51
+ )
@@ -0,0 +1,166 @@
1
+ """Agent/tool manifest posture rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ from pathlib import Path
7
+ import re
8
+ from typing import Any
9
+
10
+ from agentveil_posture.report import Finding
11
+ from agentveil_posture.rules.parsing import ParsedDocument, load_json_document, load_yaml_document
12
+
13
+
14
+ EXACT_MANIFEST_PATHS = {
15
+ ".mcp.json",
16
+ ".cursor/mcp.json",
17
+ "mcp.json",
18
+ "mcp_config.json",
19
+ }
20
+ MANIFEST_NAME_PATTERNS = (
21
+ "crew*.yaml",
22
+ "crew*.yml",
23
+ "autogen*.json",
24
+ "autogen*.yaml",
25
+ "autogen*.yml",
26
+ "langchain*.json",
27
+ "langchain*.yaml",
28
+ "langchain*.yml",
29
+ )
30
+ CREWAI_PATH_REGEX = re.compile(r"(^|/)crews/[^/]+/config/(agents|tasks)\.ya?ml$")
31
+ DIRECT_GITHUB_TOKEN_RE = re.compile(
32
+ r"\bsecrets\.(GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT)\b"
33
+ r"|^\s*[\"']?(GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT|github-token)[\"']?\s*[:=]"
34
+ r"|[\"'](GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT|github-token)[\"']\s*:",
35
+ re.IGNORECASE | re.MULTILINE,
36
+ )
37
+ SHELL_KEY_RE = re.compile(r"(^|[_-])(shell|bash|command|terminal|subprocess)([_-]|$)", re.I)
38
+ SHELL_LINE_RE = re.compile(
39
+ r"(^|[\s,{])[\"']?(shell|bash|command|terminal|subprocess)[\"']?\s*[:=]",
40
+ re.I,
41
+ )
42
+ APPROVAL_KEY_RE = re.compile(r"(approval|approve|human)", re.I)
43
+ MAX_MANIFEST_SCAN_DEPTH = 100
44
+
45
+
46
+ def is_agent_manifest(root: Path, path: Path) -> bool:
47
+ relative = path.relative_to(root).as_posix()
48
+ if relative in EXACT_MANIFEST_PATHS:
49
+ return True
50
+ if CREWAI_PATH_REGEX.search(relative):
51
+ return True
52
+ name = path.name.lower()
53
+ return any(fnmatch.fnmatch(name, pattern) for pattern in MANIFEST_NAME_PATTERNS)
54
+
55
+
56
+ def scan_manifest_rules(root: Path, path: Path) -> list[Finding]:
57
+ document = load_manifest(path)
58
+ if document is None:
59
+ return []
60
+ findings: list[Finding] = []
61
+ findings.extend(scan_manifest_direct_github_token(root, path, document))
62
+ findings.extend(scan_manifest_shell_without_approval(root, path, document))
63
+ return findings
64
+
65
+
66
+ def load_manifest(path: Path) -> ParsedDocument | None:
67
+ if path.suffix.lower() == ".json":
68
+ return load_json_document(path)
69
+ if path.suffix.lower() in {".yaml", ".yml"}:
70
+ return load_yaml_document(path)
71
+ return None
72
+
73
+
74
+ def scan_manifest_direct_github_token(
75
+ root: Path, path: Path, document: ParsedDocument
76
+ ) -> list[Finding]:
77
+ line = _first_matching_line(document.lines, DIRECT_GITHUB_TOKEN_RE)
78
+ if line is None:
79
+ return []
80
+ return [
81
+ Finding(
82
+ rule_id="bypass.direct_github_token",
83
+ severity="high",
84
+ file=path.relative_to(root).as_posix(),
85
+ line=line,
86
+ message="Agent/tool manifest appears to expose direct GitHub token access.",
87
+ remediation=(
88
+ "Remove direct GitHub tokens from agent manifests, restrict token "
89
+ "permissions, and require approval for GitHub write or deploy paths."
90
+ ),
91
+ )
92
+ ]
93
+
94
+
95
+ def scan_manifest_shell_without_approval(
96
+ root: Path, path: Path, document: ParsedDocument
97
+ ) -> list[Finding]:
98
+ try:
99
+ has_shell = _has_shell_capability(document.data)
100
+ has_approval = _has_approval_signal(document.data)
101
+ except RecursionError:
102
+ return []
103
+ if not has_shell or has_approval:
104
+ return []
105
+ return [
106
+ Finding(
107
+ rule_id="tool.shell_without_approval",
108
+ severity="high",
109
+ file=path.relative_to(root).as_posix(),
110
+ line=_first_shell_line(document.lines),
111
+ message="Agent/tool manifest appears to enable shell execution without approval.",
112
+ remediation=(
113
+ "Require explicit approval or a narrow allowlist before enabling "
114
+ "shell, terminal, command, bash, or subprocess tools."
115
+ ),
116
+ )
117
+ ]
118
+
119
+
120
+ def _has_shell_capability(value: Any, depth: int = 0) -> bool:
121
+ if depth > MAX_MANIFEST_SCAN_DEPTH:
122
+ return False
123
+ if isinstance(value, dict):
124
+ for key, child in value.items():
125
+ if SHELL_KEY_RE.search(str(key)) and _value_enabled(child):
126
+ return True
127
+ if _has_shell_capability(child, depth + 1):
128
+ return True
129
+ if isinstance(value, list):
130
+ return any(_has_shell_capability(item, depth + 1) for item in value)
131
+ return False
132
+
133
+
134
+ def _has_approval_signal(value: Any, depth: int = 0) -> bool:
135
+ if depth > MAX_MANIFEST_SCAN_DEPTH:
136
+ return False
137
+ if isinstance(value, dict):
138
+ for key, child in value.items():
139
+ if APPROVAL_KEY_RE.search(str(key)) and _value_enabled(child):
140
+ return True
141
+ if _has_approval_signal(child, depth + 1):
142
+ return True
143
+ if isinstance(value, list):
144
+ return any(_has_approval_signal(item, depth + 1) for item in value)
145
+ return False
146
+
147
+
148
+ def _value_enabled(value: Any) -> bool:
149
+ if value is None or value is False:
150
+ return False
151
+ if isinstance(value, str):
152
+ return value.strip().lower() not in {"", "false", "no", "off", "none"}
153
+ if isinstance(value, (list, dict)):
154
+ return bool(value)
155
+ return True
156
+
157
+
158
+ def _first_shell_line(lines: list[str]) -> int | None:
159
+ return _first_matching_line(lines, SHELL_LINE_RE)
160
+
161
+
162
+ def _first_matching_line(lines: list[str], pattern: re.Pattern[str]) -> int | None:
163
+ for line_number, line in enumerate(lines, start=1):
164
+ if pattern.search(line):
165
+ return line_number
166
+ return None
@@ -0,0 +1,72 @@
1
+ """Bounded parsers for untrusted scanned configuration files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+
13
+ MAX_TEXT_BYTES = 1_000_000
14
+ MAX_YAML_ALIAS_TOKENS = 25
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ParsedDocument:
19
+ data: Any
20
+ text: str
21
+ lines: list[str]
22
+
23
+
24
+ def load_yaml_document(path: Path, max_bytes: int = MAX_TEXT_BYTES) -> ParsedDocument | None:
25
+ text = read_bounded_text(path, max_bytes=max_bytes)
26
+ if text is None or has_excessive_yaml_aliases(text):
27
+ return None
28
+ try:
29
+ data = yaml.safe_load(text)
30
+ except (yaml.YAMLError, RecursionError):
31
+ return None
32
+ return ParsedDocument(data=data, text=text, lines=text.splitlines())
33
+
34
+
35
+ def load_json_document(path: Path, max_bytes: int = MAX_TEXT_BYTES) -> ParsedDocument | None:
36
+ text = read_bounded_text(path, max_bytes=max_bytes)
37
+ if text is None:
38
+ return None
39
+ try:
40
+ data = json.loads(text)
41
+ except (json.JSONDecodeError, RecursionError):
42
+ return None
43
+ return ParsedDocument(data=data, text=text, lines=text.splitlines())
44
+
45
+
46
+ def read_bounded_text(path: Path, max_bytes: int = MAX_TEXT_BYTES) -> str | None:
47
+ try:
48
+ if path.stat().st_size > max_bytes:
49
+ return None
50
+ return path.read_text(encoding="utf-8-sig")
51
+ except (OSError, UnicodeDecodeError):
52
+ return None
53
+
54
+
55
+ def has_excessive_yaml_aliases(text: str) -> bool:
56
+ aliases = 0
57
+ try:
58
+ for token in yaml.scan(text):
59
+ if isinstance(token, yaml.AliasToken):
60
+ aliases += 1
61
+ if aliases > MAX_YAML_ALIAS_TOKENS:
62
+ return True
63
+ except yaml.YAMLError:
64
+ return True
65
+ return False
66
+
67
+
68
+ def first_matching_line(lines: list[str], pattern) -> int | None:
69
+ for line_number, line in enumerate(lines, start=1):
70
+ if pattern.search(line):
71
+ return line_number
72
+ return None
@@ -0,0 +1,182 @@
1
+ """GitHub workflow posture rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import re
7
+
8
+ from agentveil_posture.report import Finding
9
+ from agentveil_posture.rules.parsing import (
10
+ MAX_TEXT_BYTES as MAX_WORKFLOW_BYTES,
11
+ MAX_YAML_ALIAS_TOKENS,
12
+ ParsedDocument,
13
+ load_yaml_document,
14
+ )
15
+
16
+
17
+ DEPLOY_MARKER_RE = re.compile(
18
+ r"\b(deploy|deployment|release|kubectl)\b"
19
+ r"|\b(npm|pnpm|yarn|pypi|twine|poetry)\s+publish\b"
20
+ r"|\bterraform\s+apply\b"
21
+ r"|\bcloudformation\s+deploy\b"
22
+ r"|\bserverless\s+deploy\b",
23
+ re.IGNORECASE,
24
+ )
25
+ BUILD_CONFIG_EXCLUSIONS = re.compile(
26
+ r"--configuration\s+\w+"
27
+ r"|--framework\s+\w+"
28
+ r"|\brelease\s+notes\b"
29
+ r"|\brelease\s+candidate\b",
30
+ re.IGNORECASE,
31
+ )
32
+ APPROVAL_MARKERS = ("approval", "manual approval", "review", "protected environment")
33
+ PULL_REQUEST_TARGET_RE = re.compile(r"(^|\s)pull_request_target\s*:", re.MULTILINE)
34
+ PULL_REQUEST_TARGET_INLINE_RE = re.compile(
35
+ r"^\s*on\s*:\s*(\[.*pull_request_target.*\]|pull_request_target)\s*$",
36
+ re.MULTILINE,
37
+ )
38
+ PULL_REQUEST_TARGET_LIST_RE = re.compile(r"^\s*-\s*pull_request_target\s*$", re.MULTILINE)
39
+ DIRECT_GITHUB_TOKEN_RE = re.compile(
40
+ r"\bsecrets\.(GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT)\b"
41
+ r"|^\s*(GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT|github-token)\s*:",
42
+ re.IGNORECASE | re.MULTILINE,
43
+ )
44
+
45
+
46
+ def scan_workflow_rules(root: Path, path: Path) -> list[Finding]:
47
+ document = load_workflow(path)
48
+ if document is None:
49
+ return []
50
+ findings: list[Finding] = []
51
+ findings.extend(scan_workflow_direct_github_token(root, path, document))
52
+ findings.extend(scan_workflow_deploy_without_approval(root, path, document))
53
+ findings.extend(scan_workflow_pull_request_target_secrets_risk(root, path, document))
54
+ return findings
55
+
56
+
57
+ def scan_workflow_direct_github_token(
58
+ root: Path, path: Path, document: ParsedDocument
59
+ ) -> list[Finding]:
60
+ line = _first_matching_line(document.lines, DIRECT_GITHUB_TOKEN_RE)
61
+ if line is None:
62
+ return []
63
+ return [
64
+ Finding(
65
+ rule_id="bypass.direct_github_token",
66
+ severity="high",
67
+ file=path.relative_to(root).as_posix(),
68
+ line=line,
69
+ message="Workflow appears to expose direct GitHub token access.",
70
+ remediation=(
71
+ "Restrict GitHub token permissions, avoid passing direct write "
72
+ "tokens to agent-controlled steps, and require approval for "
73
+ "GitHub write or deploy paths."
74
+ ),
75
+ )
76
+ ]
77
+
78
+
79
+ def scan_workflow_deploy_without_approval(
80
+ root: Path, path: Path, document: ParsedDocument
81
+ ) -> list[Finding]:
82
+ deploy_line = _first_deploy_line(document.lines)
83
+ if deploy_line is None or _has_approval_signal(document.text):
84
+ return []
85
+ return [
86
+ Finding(
87
+ rule_id="workflow.deploy_without_approval",
88
+ severity="high",
89
+ file=path.relative_to(root).as_posix(),
90
+ line=deploy_line,
91
+ message="Deployment workflow appears to run without an approval gate.",
92
+ remediation=(
93
+ "Add a protected GitHub environment or explicit manual approval "
94
+ "before production deploy, release, or publish steps."
95
+ ),
96
+ )
97
+ ]
98
+
99
+
100
+ def scan_workflow_pull_request_target_secrets_risk(
101
+ root: Path, path: Path, document: ParsedDocument
102
+ ) -> list[Finding]:
103
+ trigger_line = _pull_request_target_line(document.lines)
104
+ if trigger_line is None or not _has_pull_request_target_risk(document.text):
105
+ return []
106
+ return [
107
+ Finding(
108
+ rule_id="workflow.pull_request_target_secrets_risk",
109
+ severity="high",
110
+ file=path.relative_to(root).as_posix(),
111
+ line=trigger_line,
112
+ message=(
113
+ "pull_request_target workflow appears to combine privileged PR "
114
+ "context with checkout, shell execution, or secrets access."
115
+ ),
116
+ remediation=(
117
+ "Use pull_request for untrusted checks, avoid checking out fork "
118
+ "code in privileged workflows, and isolate any secret-bearing jobs."
119
+ ),
120
+ )
121
+ ]
122
+
123
+
124
+ def load_workflow(path: Path) -> ParsedDocument | None:
125
+ return load_yaml_document(path, max_bytes=MAX_WORKFLOW_BYTES)
126
+
127
+
128
+ def _first_deploy_line(lines: list[str]) -> int | None:
129
+ for line_number, line in enumerate(lines, start=1):
130
+ if BUILD_CONFIG_EXCLUSIONS.search(line):
131
+ continue
132
+ if DEPLOY_MARKER_RE.search(line):
133
+ return line_number
134
+ return None
135
+
136
+
137
+ def _first_matching_line(lines: list[str], pattern: re.Pattern[str]) -> int | None:
138
+ for line_number, line in enumerate(lines, start=1):
139
+ if pattern.search(line):
140
+ return line_number
141
+ return None
142
+
143
+
144
+ def _has_approval_signal(text: str) -> bool:
145
+ normalized = text.lower()
146
+ if re.search(r"^\s*environment\s*:", text, flags=re.MULTILINE):
147
+ return True
148
+ return any(marker in normalized for marker in APPROVAL_MARKERS)
149
+
150
+
151
+ def _pull_request_target_line(lines: list[str]) -> int | None:
152
+ for line_number, line in enumerate(lines, start=1):
153
+ if line.lstrip().startswith("#"):
154
+ continue
155
+ if "pull_request_target" in line:
156
+ return line_number
157
+ return None
158
+
159
+
160
+ def _has_pull_request_target_risk(text: str) -> bool:
161
+ if not _has_pull_request_target_trigger(text):
162
+ return False
163
+ normalized = text.lower()
164
+ return (
165
+ "actions/checkout" in normalized
166
+ or "${{ secrets." in normalized
167
+ or re.search(r"^\s*-?\s*run\s*:", text, flags=re.MULTILINE) is not None
168
+ or "github.event.pull_request.head" in normalized
169
+ )
170
+
171
+
172
+ def _has_pull_request_target_trigger(text: str) -> bool:
173
+ text = _without_comment_lines(text)
174
+ return (
175
+ PULL_REQUEST_TARGET_RE.search(text) is not None
176
+ or PULL_REQUEST_TARGET_INLINE_RE.search(text) is not None
177
+ or PULL_REQUEST_TARGET_LIST_RE.search(text) is not None
178
+ )
179
+
180
+
181
+ def _without_comment_lines(text: str) -> str:
182
+ return "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
@@ -0,0 +1,61 @@
1
+ """Static scan orchestrator for AgentVeil Posture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from agentveil_posture.report import Finding, PostureReport, build_report
9
+ from agentveil_posture.rules import (
10
+ is_agent_manifest,
11
+ scan_identity_private_key_unencrypted,
12
+ scan_manifest_rules,
13
+ scan_workflow_rules,
14
+ )
15
+
16
+
17
+ class ScanError(Exception):
18
+ """Raised when the requested scan path cannot be scanned safely."""
19
+
20
+
21
+ def scan_path(path: Path) -> PostureReport:
22
+ """Run a static, read-only scan for the currently implemented v0.1 rules."""
23
+ root = path.resolve()
24
+ if not root.exists():
25
+ raise ScanError(f"scan path does not exist: {path}")
26
+ if not root.is_dir():
27
+ raise ScanError(f"scan path is not a directory: {path}")
28
+
29
+ findings: list[Finding] = []
30
+ for candidate in _iter_regular_files(root):
31
+ finding = scan_identity_private_key_unencrypted(root, candidate)
32
+ if finding is not None:
33
+ findings.append(finding)
34
+ if _is_github_workflow(root, candidate):
35
+ findings.extend(scan_workflow_rules(root, candidate))
36
+ if is_agent_manifest(root, candidate):
37
+ findings.extend(scan_manifest_rules(root, candidate))
38
+
39
+ return build_report(str(root), findings)
40
+
41
+
42
+ def _iter_regular_files(root: Path) -> list[Path]:
43
+ paths: list[Path] = []
44
+ for dirpath, _dirnames, filenames in os.walk(str(root), followlinks=False):
45
+ for filename in filenames:
46
+ path = Path(dirpath) / filename
47
+ if path.is_symlink():
48
+ continue
49
+ if path.is_file():
50
+ paths.append(path)
51
+ return sorted(paths)
52
+
53
+
54
+ def _is_github_workflow(root: Path, path: Path) -> bool:
55
+ relative = path.relative_to(root)
56
+ return (
57
+ len(relative.parts) >= 3
58
+ and relative.parts[0] == ".github"
59
+ and relative.parts[1] == "workflows"
60
+ and path.suffix.lower() in {".yml", ".yaml"}
61
+ )
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentveil-posture
3
+ Version: 0.1.0
4
+ Summary: Pre-deployment posture checks for risky AI agent capabilities.
5
+ Author: AgentVeil Protocol contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://agentveil.dev
8
+ Project-URL: Repository, https://github.com/agentveil-protocol/agentveil-posture
9
+ Project-URL: Issues, https://github.com/agentveil-protocol/agentveil-posture/issues
10
+ Project-URL: Documentation, https://github.com/agentveil-protocol/agentveil-posture#readme
11
+ Keywords: security,agent,mcp,posture,github-actions,ai-agents
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Security
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: PyYAML<7,>=6.0.1
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8; extra == "test"
28
+ Dynamic: license-file
29
+
30
+ # AgentVeil Posture
31
+
32
+ <p align="center">
33
+ <img src="docs/agentveil-posture-logo.png" alt="AgentVeil Posture logo" width="180">
34
+ </p>
35
+
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE)
37
+ [![PyPI](https://img.shields.io/pypi/v/agentveil-posture?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/agentveil-posture/)
38
+ [![Self Test](https://github.com/agentveil-protocol/agentveil-posture/actions/workflows/posture-self-test.yml/badge.svg)](https://github.com/agentveil-protocol/agentveil-posture/actions/workflows/posture-self-test.yml)
39
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/)
40
+ [![GitHub stars](https://img.shields.io/github/stars/agentveil-protocol/agentveil-posture?style=for-the-badge&logo=github&color=gold)](https://github.com/agentveil-protocol/agentveil-posture/stargazers)
41
+ [![GitHub Action](https://img.shields.io/badge/GitHub-Action_ready-2088FF?style=for-the-badge&logo=github-actions&logoColor=white)](#use-as-a-github-action)
42
+ [![Posture: read-only](https://img.shields.io/badge/scanner-read--only-10b981?style=for-the-badge)](#hard-constraints)
43
+
44
+ **Pre-deployment posture check for AI agents. Find risky capabilities before they become production incidents.**
45
+
46
+ `agentveil-posture` is a pre-deployment, static, local-only scanner that flags
47
+ risky AI-agent and GitHub-workflow posture issues. No telemetry, no network
48
+ calls, no project code execution. v0.1 ships five high-severity GitHub-focused
49
+ rules.
50
+
51
+ [Quick Start](#quick-start) |
52
+ [What a finding looks like](#what-a-finding-looks-like) |
53
+ [Detection scope](#detection-scope-v01) |
54
+ [GitHub Action](#use-as-a-github-action) |
55
+ [Why this exists](#why-this-exists)
56
+
57
+ ---
58
+
59
+ ## Quick Start
60
+
61
+ ```bash
62
+ pip install agentveil-posture
63
+ agentveil posture scan --path . --output report.json
64
+ cat report.json
65
+ ```
66
+
67
+ That is the whole flow. The scanner is read-only: it does not modify your
68
+ files, run your code, or send data over the network.
69
+
70
+ To fail CI when findings meet a threshold, add `--fail-on`:
71
+
72
+ ```bash
73
+ agentveil posture scan --path . --output report.json --fail-on high
74
+ ```
75
+
76
+ ## What a Finding Looks Like
77
+
78
+ ```json
79
+ {
80
+ "rule_id": "workflow.deploy_without_approval",
81
+ "severity": "high",
82
+ "file": ".github/workflows/deploy.yml",
83
+ "line": 12,
84
+ "message": "Deployment workflow appears to run without an approval gate.",
85
+ "remediation": "Add a protected GitHub environment or explicit manual approval before production deploy, release, or publish steps."
86
+ }
87
+ ```
88
+
89
+ Every finding contains rule ID, severity, repository-relative file path, line
90
+ number when available, redacted message, and remediation pointer. Raw secrets,
91
+ command bodies, and key material never appear in the report.
92
+
93
+ ## Detection Scope (v0.1)
94
+
95
+ All v0.1 rules are reported as `high` severity.
96
+
97
+ | Rule | What it flags |
98
+ |---|---|
99
+ | `bypass.direct_github_token` | Direct GitHub PAT/token references in workflows or agent manifests |
100
+ | `workflow.deploy_without_approval` | Deploy/release/publish steps without an approval gate |
101
+ | `workflow.pull_request_target_secrets_risk` | `pull_request_target` workflows that combine privileged context with checkout, run, or secrets |
102
+ | `tool.shell_without_approval` | Agent tool manifests that enable shell execution without an approval flag |
103
+ | `identity.private_key_unencrypted` | Unencrypted PEM private key files committed to the repo |
104
+
105
+ ## Install
106
+
107
+ <details>
108
+ <summary><b>From PyPI (recommended)</b></summary>
109
+
110
+ ```bash
111
+ pip install agentveil-posture
112
+ ```
113
+
114
+ </details>
115
+
116
+ <details>
117
+ <summary><b>From GitHub release</b></summary>
118
+
119
+ ```bash
120
+ pip install git+https://github.com/agentveil-protocol/agentveil-posture@v0.1.0
121
+ ```
122
+
123
+ </details>
124
+
125
+ <details>
126
+ <summary><b>From source (development)</b></summary>
127
+
128
+ ```bash
129
+ git clone https://github.com/agentveil-protocol/agentveil-posture
130
+ cd agentveil-posture
131
+ pip install -e .
132
+ ```
133
+
134
+ </details>
135
+
136
+ ## Use as a GitHub Action
137
+
138
+ Use the action from the same repository:
139
+
140
+ ```yaml
141
+ - uses: agentveil-protocol/agentveil-posture@v0.1.0
142
+ with:
143
+ path: "."
144
+ output: agentveil-posture-report.json
145
+ fail-on: high
146
+ ```
147
+
148
+ The action requires Python 3.10 or newer on the runner. It writes the report
149
+ path to the `report` output and does not upload data to AgentVeil. Omit
150
+ `fail-on` to keep review-only behavior.
151
+
152
+ For GitHub Code Scanning, write SARIF and upload it with CodeQL:
153
+
154
+ ```yaml
155
+ - uses: agentveil-protocol/agentveil-posture@v0.1.0
156
+ with:
157
+ path: "."
158
+ output: agentveil-posture.sarif
159
+ format: sarif
160
+
161
+ - uses: github/codeql-action/upload-sarif@v3
162
+ with:
163
+ sarif_file: agentveil-posture.sarif
164
+ ```
165
+
166
+ ## Pre-commit Hook
167
+
168
+ Run AgentVeil Posture as a [pre-commit](https://pre-commit.com) hook to catch
169
+ posture issues before they reach the remote.
170
+
171
+ Add to your `.pre-commit-config.yaml`:
172
+
173
+ ```yaml
174
+ repos:
175
+ - repo: https://github.com/agentveil-protocol/agentveil-posture
176
+ rev: v0.1.0
177
+ hooks:
178
+ - id: agentveil-posture
179
+ args: ["--fail-on", "high"]
180
+ ```
181
+
182
+ Then install:
183
+
184
+ ```bash
185
+ pre-commit install
186
+ ```
187
+
188
+ The hook generates `agentveil-posture-report.json` on every commit. Omit
189
+ `args` for review-only behavior, or use `--fail-on` to block commits when
190
+ findings meet the selected threshold.
191
+
192
+ ## Triaging Findings
193
+
194
+ `agentveil-posture` flags **posture surfaces**: places where an AI agent or
195
+ workflow has direct capability to do something risky. Most findings are
196
+ **review items**, not incidents:
197
+
198
+ - **`bypass.direct_github_token`** commonly appears on stale-bots,
199
+ release-bots, CI publish steps, and label-management workflows that
200
+ legitimately use the auto-injected `secrets.GITHUB_TOKEN`. The rule fires
201
+ by design: the workflow holds direct GitHub write capability and that is a
202
+ posture surface worth surfacing, even when expected.
203
+ - **`workflow.deploy_without_approval`** may flag deploy paths that have
204
+ approval mechanisms the static scanner cannot see, such as manual job
205
+ dispatch, branch protection, or external reviewer chains. Verify against
206
+ your actual approval flow before treating as incident.
207
+ - **`workflow.pull_request_target_secrets_risk`** flags risky combinations,
208
+ but some `pull_request_target` workflows are correctly scoped to label-only
209
+ or metadata-only operations. Re-check the actual job content.
210
+ - **`tool.shell_without_approval`** flags inline shell capability
211
+ declarations. Tools referenced by name, such as `search_tool` in CrewAI,
212
+ are not detected; only literal `shell:` or `bash:` keys are.
213
+ - **`identity.private_key_unencrypted`** is the most reliably actionable
214
+ finding: committed unencrypted private keys are usually real issues.
215
+
216
+ Use posture-check to surface review items for human triage, not to auto-block
217
+ CI or replace SAST/secret-scanning tools.
218
+
219
+ ## Why This Exists
220
+
221
+ AI agents increasingly touch production credentials, deploy workflows, and
222
+ developer infrastructure. AgentVeil Posture is the first step: find risky
223
+ capabilities before deployment and before they become incidents.
224
+
225
+ ```text
226
+ +----------+ +----------+ +----------+
227
+ | FIND | | DECIDE | | PROVE |
228
+ | risky | ---> | what is | ---> | what |
229
+ | caps | | allowed | | happened |
230
+ +----------+ +----------+ +----------+
231
+ you are here roadmap roadmap
232
+ v0.1 Posture
233
+ ```
234
+
235
+ | | Posture does | Posture does not |
236
+ |---|---|---|
237
+ | Scope | Static analysis and posture risk patterns | Approval, blocking, or execution of agent actions |
238
+ | Effects | Read-only file inspection | Code execution, network calls, or file mutation |
239
+ | Output | Redacted JSON findings | Secret values, command bodies, or key bytes |
240
+
241
+ For the broader AgentVeil project, see [agentveil.dev](https://agentveil.dev).
242
+
243
+ ## Hard Constraints
244
+
245
+ The scanner is designed to be:
246
+
247
+ - **offline**: no network calls
248
+ - **telemetry-free**: no usage data collected
249
+ - **read-only**: does not modify scanned files
250
+ - **static-only**: does not execute scanned project code
251
+ - **secret-safe**: reports only redacted findings
252
+
253
+ Private-key checks use file metadata and bounded header sniffing only.
254
+
255
+ ## Dependency Policy
256
+
257
+ Runtime dependencies are intentionally minimal:
258
+
259
+ - Python `>=3.10`
260
+ - `PyYAML>=6.0.1,<7`
261
+
262
+ Additional runtime dependencies require explicit justification in
263
+ [PLAN.md](PLAN.md).
264
+
265
+ ## Known Limitations
266
+
267
+ `agentveil-posture` v0.1 is a best-effort heuristic scanner, not an exhaustive
268
+ security audit.
269
+
270
+ - Some rules may produce false positives or false negatives.
271
+ - Oversized, unreadable, or malformed inputs may be skipped without per-file
272
+ skip reasons.
273
+ - YAML parsing is bounded, but carefully crafted YAML within the v0.1 alias
274
+ limit can still consume parser memory.
275
+ - The repository includes an intentional synthetic PEM-shaped fixture for
276
+ scanner tests. It is not a real private key.
277
+
278
+ ## Community
279
+
280
+ - [Star this repo](https://github.com/agentveil-protocol/agentveil-posture/stargazers)
281
+ if Posture helps your team.
282
+ - [Open an issue](https://github.com/agentveil-protocol/agentveil-posture/issues)
283
+ for bugs, false positives, or rule suggestions.
284
+ - See [PLAN.md](PLAN.md) for the v0.1 spec, schema details, and v0.2 backlog.
285
+
286
+ ## License
287
+
288
+ MIT. See [LICENSE](LICENSE).
289
+
290
+ ---
291
+
292
+ Part of the [AgentVeil project](https://agentveil.dev): action control for
293
+ autonomous agents.
@@ -0,0 +1,15 @@
1
+ agentveil_posture/__init__.py,sha256=Dgw7FEUaYoFV0C4WHG2n5w5SYQoIIbIOiA8a-zFsABI,91
2
+ agentveil_posture/cli.py,sha256=kWraymEt1oCQXPSs63nbl_cXcTfEnn8wxSfjcthxwB4,2883
3
+ agentveil_posture/report.py,sha256=O6WX-aCQsPbQs_XWxLzELe9VW6IsLNRUuQl_03NWDsE,7032
4
+ agentveil_posture/scanner.py,sha256=v4zhZAHi-f-ChtUeaTn8l37bd2etIvB74FUQdU-arlc,1967
5
+ agentveil_posture/rules/__init__.py,sha256=hXCYfGaAhdYVAjj1MmSD5EG8gRssRiftQmGv9bGqTNQ,581
6
+ agentveil_posture/rules/identity.py,sha256=Ja--Wi0pYpUkU-itZhA1LWZxqfRUKsOF2CFsnHrzJYo,1544
7
+ agentveil_posture/rules/manifest.py,sha256=ntzVosGBk4NHuhcrlSlFBhsGiqiW6F8Ug4dU1_dYoBc,5441
8
+ agentveil_posture/rules/parsing.py,sha256=JjJMCpuZOpQlc17zDjdn3t2EaliZT2CQlF-PE0O2jl8,1975
9
+ agentveil_posture/rules/workflow.py,sha256=D_XcjNPtC2d8ytRPlQoC94naAA7Sm9LOJV5MbcF056c,6165
10
+ agentveil_posture-0.1.0.dist-info/licenses/LICENSE,sha256=tINj2Q4xPZbm90mBF98TA4FWxlpmvcKGrOh8vLF0f64,1088
11
+ agentveil_posture-0.1.0.dist-info/METADATA,sha256=ihh3rHGupC_xRHDvNubSRPbqPVVdni7rZWT9pXXDgQ8,10515
12
+ agentveil_posture-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ agentveil_posture-0.1.0.dist-info/entry_points.txt,sha256=n7anDKPyXHTY6khIeNX3v53bu5BvcNAVaa7VCUxv4hA,57
14
+ agentveil_posture-0.1.0.dist-info/top_level.txt,sha256=O9TZzoSTHqDrSDHjG9za84UBuqgDfOoIpPrPpUoGras,18
15
+ agentveil_posture-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentveil = agentveil_posture.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentVeil Protocol contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ agentveil_posture