stacktrace-cli 0.0.1__py3-none-any.whl → 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.
- stacktrace_cli/__init__.py +1 -1
- stacktrace_cli/__main__.py +10 -18
- stacktrace_cli/cli.py +314 -0
- stacktrace_cli/correlate/__init__.py +1 -0
- stacktrace_cli/correlate/acquire.py +373 -0
- stacktrace_cli/correlate/composition.py +375 -0
- stacktrace_cli/correlate/join.py +380 -0
- stacktrace_cli/correlate/observed.py +1836 -0
- stacktrace_cli/correlate/orchestrate.py +417 -0
- stacktrace_cli/correlate/project_map.py +81 -0
- stacktrace_cli/correlate/record.py +279 -0
- stacktrace_cli/correlate/render.py +766 -0
- stacktrace_cli/detector/__init__.py +1 -0
- stacktrace_cli/detector/analyzer.py +207 -0
- stacktrace_cli/detector/cache.py +483 -0
- stacktrace_cli/detector/deterministic.py +606 -0
- stacktrace_cli/detector/finding.py +247 -0
- stacktrace_cli/detector/markers.py +158 -0
- stacktrace_cli/detector/priors.py +502 -0
- stacktrace_cli/detector/prompts/__init__.py +36 -0
- stacktrace_cli/detector/prompts/v1/exclusions.md +36 -0
- stacktrace_cli/detector/prompts/v1/framing.md +23 -0
- stacktrace_cli/detector/prompts/v1/stacktrace-deceptive-completion.md +12 -0
- stacktrace_cli/detector/prompts/v1/stacktrace-injected-instruction-followed.md +14 -0
- stacktrace_cli/detector/prompts/v1/stacktrace-intent-drift.md +11 -0
- stacktrace_cli/detector/reasoning.py +1180 -0
- stacktrace_cli/detector/render.py +310 -0
- stacktrace_cli/detector/rules.py +136 -0
- stacktrace_cli/detector/run.py +453 -0
- stacktrace_cli/detector/secrets.py +203 -0
- stacktrace_cli/detector/verdict.py +266 -0
- stacktrace_cli/remote/__init__.py +1 -0
- stacktrace_cli/remote/cli.py +475 -0
- stacktrace_cli/remote/client.py +393 -0
- stacktrace_cli/remote/config.py +106 -0
- stacktrace_cli/remote/detect_payload.py +262 -0
- stacktrace_cli/remote/payload.py +435 -0
- stacktrace_cli/remote/policy.py +88 -0
- stacktrace_cli/remote/redact.py +649 -0
- stacktrace_cli/remote/spool.py +446 -0
- stacktrace_cli/remote/sync.py +558 -0
- stacktrace_cli/remote/sync_detect.py +409 -0
- stacktrace_cli/remote/upload_contract.py +973 -0
- stacktrace_cli/sessions/__init__.py +1 -0
- stacktrace_cli/sessions/access.py +79 -0
- stacktrace_cli/sessions/outcome.py +65 -0
- stacktrace_cli/sessions/protocols.py +204 -0
- stacktrace_cli/sessions/render.py +295 -0
- stacktrace_cli-0.1.0.dist-info/METADATA +228 -0
- stacktrace_cli-0.1.0.dist-info/RECORD +52 -0
- stacktrace_cli-0.0.1.dist-info/METADATA +0 -39
- stacktrace_cli-0.0.1.dist-info/RECORD +0 -6
- {stacktrace_cli-0.0.1.dist-info → stacktrace_cli-0.1.0.dist-info}/WHEEL +0 -0
- {stacktrace_cli-0.0.1.dist-info → stacktrace_cli-0.1.0.dist-info}/entry_points.txt +0 -0
stacktrace_cli/__init__.py
CHANGED
stacktrace_cli/__main__.py
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
"""Entry point for the ``stacktrace`` command.
|
|
1
|
+
"""Entry point for the ``stacktrace`` command.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
HOMEPAGE = "https://stacktrace.ai"
|
|
3
|
+
`[project.scripts]` still names `stacktrace_cli.__main__:main`, which now
|
|
4
|
+
resolves to the Click group in `stacktrace_cli.cli`, so both the console script
|
|
5
|
+
and `python -m stacktrace_cli` reach the same object and the published
|
|
6
|
+
entry-point string does not change between releases.
|
|
7
|
+
"""
|
|
10
8
|
|
|
9
|
+
from __future__ import annotations
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
parser = argparse.ArgumentParser(
|
|
14
|
-
prog="stacktrace",
|
|
15
|
-
description=f"Stacktrace.ai CLI (pre-alpha). See {HOMEPAGE}",
|
|
16
|
-
)
|
|
17
|
-
parser.add_argument("--version", action="version", version=f"stacktrace {__version__}")
|
|
18
|
-
parser.parse_args(argv)
|
|
19
|
-
print(f"stacktrace {__version__} — pre-alpha placeholder. See {HOMEPAGE}")
|
|
20
|
-
return 0
|
|
11
|
+
from .cli import main
|
|
21
12
|
|
|
13
|
+
__all__ = ["main"]
|
|
22
14
|
|
|
23
15
|
if __name__ == "__main__":
|
|
24
|
-
|
|
16
|
+
main()
|
stacktrace_cli/cli.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""The ``stacktrace`` command group.
|
|
2
|
+
|
|
3
|
+
Two kinds of command live here (ADR-0021, `docs/adrs/0021-two-command-kinds.md`):
|
|
4
|
+
|
|
5
|
+
- **Pass-through** — OpenACA's own Click command object, mounted under this
|
|
6
|
+
group. It declares no parameter of its own, ever, and nothing here parses its
|
|
7
|
+
arguments; Click dispatches them to OpenACA's parser. The object registered
|
|
8
|
+
under ``stacktrace scan`` *is* ``openaca scan``.
|
|
9
|
+
- **Native** — a command this package implements, registered the ordinary way.
|
|
10
|
+
If it needs OpenACA it calls ``openaca.core`` (ADR-0020).
|
|
11
|
+
|
|
12
|
+
`openaca.cli` is a supported import path published for exactly this use
|
|
13
|
+
(ADR-0020, `docs/adrs/0020-openaca-consumption-boundary.md`).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from datetime import UTC, datetime, timedelta
|
|
20
|
+
from importlib.metadata import version
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Final
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
from openaca.cli import main as openaca_cli
|
|
26
|
+
|
|
27
|
+
from . import __version__
|
|
28
|
+
from .correlate.orchestrate import acquire_correlated_view
|
|
29
|
+
from .correlate.project_map import parse_mapping
|
|
30
|
+
from .detector.cache import VerdictCache, default_directory
|
|
31
|
+
from .detector.render import render_json as render_detections_json
|
|
32
|
+
from .detector.render import render_text as render_detections_text
|
|
33
|
+
from .detector.run import DEFAULT_BUDGET, DEFAULT_SAMPLE_BUDGET, run_detector
|
|
34
|
+
from .remote.cli import main as remote_cmd
|
|
35
|
+
from .sessions.access import collect_sessions
|
|
36
|
+
from .sessions.render import render_json, render_text
|
|
37
|
+
|
|
38
|
+
PASSTHROUGH: Final = ("scan", "bom", "policy")
|
|
39
|
+
|
|
40
|
+
HOMEPAGE = "https://stacktrace.ai"
|
|
41
|
+
|
|
42
|
+
_SINCE_PATTERN = re.compile(r"^(\d+)d$")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_since(value: str) -> datetime:
|
|
46
|
+
"""`14d` -> an aware UTC cut-off."""
|
|
47
|
+
match = _SINCE_PATTERN.match(value.strip())
|
|
48
|
+
if not match:
|
|
49
|
+
raise click.BadParameter(f"expected a day count like '14d', got {value!r}")
|
|
50
|
+
return datetime.now(UTC) - timedelta(days=int(match.group(1)))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _show_version(ctx: click.Context, param: click.Parameter, value: bool) -> None:
|
|
54
|
+
# Click invokes an eager option's callback on every invocation of the
|
|
55
|
+
# group, with `value=False` when the flag was absent, so an unguarded
|
|
56
|
+
# callback would print the version and exit before any subcommand ran.
|
|
57
|
+
# click's own `version_option` callback opens with the same two guards.
|
|
58
|
+
if not value or ctx.resilient_parsing:
|
|
59
|
+
return
|
|
60
|
+
click.echo(f"stacktrace {__version__} (openaca {version('openaca')})")
|
|
61
|
+
ctx.exit()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SectionedGroup(click.Group):
|
|
65
|
+
"""A group whose command listing is split into delegated and native sections.
|
|
66
|
+
|
|
67
|
+
Click has no notion of command sections and the distinction matters here: a
|
|
68
|
+
delegated command's own ``--help`` describes OpenACA and its options are
|
|
69
|
+
OpenACA's, which a flat alphabetical listing tells nobody. The partition is
|
|
70
|
+
derived from `PASSTHROUGH`, so registering a native command puts it in the
|
|
71
|
+
right section with no edit to this class.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
75
|
+
commands: list[tuple[str, click.Command]] = []
|
|
76
|
+
for name in self.list_commands(ctx):
|
|
77
|
+
cmd = self.get_command(ctx, name)
|
|
78
|
+
if cmd is None or cmd.hidden:
|
|
79
|
+
continue
|
|
80
|
+
commands.append((name, cmd))
|
|
81
|
+
if not commands:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
limit = formatter.width - 6 - max(len(name) for name, _ in commands)
|
|
85
|
+
delegated = [
|
|
86
|
+
(name, cmd.get_short_help_str(limit)) for name, cmd in commands if name in PASSTHROUGH
|
|
87
|
+
]
|
|
88
|
+
native = [
|
|
89
|
+
(name, cmd.get_short_help_str(limit))
|
|
90
|
+
for name, cmd in commands
|
|
91
|
+
if name not in PASSTHROUGH
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for heading, rows in (("Analysis (OpenACA)", delegated), ("Stacktrace", native)):
|
|
95
|
+
if not rows:
|
|
96
|
+
continue
|
|
97
|
+
with formatter.section(heading):
|
|
98
|
+
formatter.write_dl(rows)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@click.group(cls=SectionedGroup)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--version",
|
|
104
|
+
is_flag=True,
|
|
105
|
+
expose_value=False,
|
|
106
|
+
is_eager=True,
|
|
107
|
+
callback=_show_version,
|
|
108
|
+
help="Show the stacktrace and OpenACA versions and exit.",
|
|
109
|
+
)
|
|
110
|
+
def main() -> None:
|
|
111
|
+
"""Stacktrace: agent composition analysis, detection and upload."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@click.command()
|
|
115
|
+
@click.option(
|
|
116
|
+
"--agent-kind",
|
|
117
|
+
"agent_kinds",
|
|
118
|
+
multiple=True,
|
|
119
|
+
help="Only this agent kind. Repeatable. Default: every kind.",
|
|
120
|
+
)
|
|
121
|
+
@click.option("--since", default="14d", show_default=True, help="Window, as a day count.")
|
|
122
|
+
@click.option(
|
|
123
|
+
"--include-content",
|
|
124
|
+
is_flag=True,
|
|
125
|
+
default=False,
|
|
126
|
+
help="Include prompts, tool arguments and results. Off by default: a rendered "
|
|
127
|
+
"document leaves the process, and the Correlator reads the model directly.",
|
|
128
|
+
)
|
|
129
|
+
@click.option(
|
|
130
|
+
"--format",
|
|
131
|
+
"output_format",
|
|
132
|
+
type=click.Choice(["text", "json"]),
|
|
133
|
+
default="text",
|
|
134
|
+
show_default=True,
|
|
135
|
+
)
|
|
136
|
+
@click.option(
|
|
137
|
+
"--detail",
|
|
138
|
+
is_flag=True,
|
|
139
|
+
default=False,
|
|
140
|
+
help="Show the session trail. Default: the summary alone.",
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"--root",
|
|
144
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
145
|
+
default=None,
|
|
146
|
+
help="Read agent transcripts from here instead of the default location.",
|
|
147
|
+
)
|
|
148
|
+
def sessions(
|
|
149
|
+
agent_kinds: tuple[str, ...],
|
|
150
|
+
since: str,
|
|
151
|
+
output_format: str,
|
|
152
|
+
include_content: bool,
|
|
153
|
+
detail: bool,
|
|
154
|
+
root: Path | None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Print what the agents on this machine actually did."""
|
|
157
|
+
view = collect_sessions(list(agent_kinds) or None, _parse_since(since), root=root)
|
|
158
|
+
output = (
|
|
159
|
+
render_json(view, include_content)
|
|
160
|
+
if output_format == "json"
|
|
161
|
+
else render_text(view, include_content, detail)
|
|
162
|
+
)
|
|
163
|
+
click.echo(output, nl=not output.endswith("\n"))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@click.command()
|
|
167
|
+
@click.option("--agent-kind", "agent_kinds", multiple=True, help="Limit to these agent kinds.")
|
|
168
|
+
@click.option("--since", default="7d", show_default=True, help="How far back to read.")
|
|
169
|
+
@click.option(
|
|
170
|
+
"--bom",
|
|
171
|
+
"bom_paths",
|
|
172
|
+
multiple=True,
|
|
173
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
174
|
+
help="Use this Agent BOM instead of scanning for its kind.",
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--format",
|
|
178
|
+
"output_format",
|
|
179
|
+
type=click.Choice(["text", "json"]),
|
|
180
|
+
default="text",
|
|
181
|
+
show_default=True,
|
|
182
|
+
)
|
|
183
|
+
@click.option(
|
|
184
|
+
"--detail",
|
|
185
|
+
is_flag=True,
|
|
186
|
+
default=False,
|
|
187
|
+
help="Show every detection and all of its evidence. Default: the summary alone.",
|
|
188
|
+
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--escalate/--no-escalate",
|
|
191
|
+
default=True,
|
|
192
|
+
show_default=True,
|
|
193
|
+
help="Send flagged sessions to the agent's own CLI for semantic analysis. On "
|
|
194
|
+
"by default, capped by --budget. It is the only stage that sends anything "
|
|
195
|
+
"session-derived off this machine, and it spends the developer's own "
|
|
196
|
+
"provider quota, so --no-escalate turns it off and the cap keeps a noisy "
|
|
197
|
+
"day bounded. It does not make the run offline: correlation runs first "
|
|
198
|
+
"either way and matches advisories by sending the package coordinates of "
|
|
199
|
+
"the components a session invoked to osv.dev.",
|
|
200
|
+
)
|
|
201
|
+
@click.option(
|
|
202
|
+
"--budget",
|
|
203
|
+
type=click.IntRange(min=1),
|
|
204
|
+
default=DEFAULT_BUDGET,
|
|
205
|
+
show_default=True,
|
|
206
|
+
help="How many sessions one run may send for inference.",
|
|
207
|
+
)
|
|
208
|
+
@click.option(
|
|
209
|
+
"--sample-budget",
|
|
210
|
+
type=click.IntRange(min=0),
|
|
211
|
+
default=DEFAULT_SAMPLE_BUDGET,
|
|
212
|
+
show_default=True,
|
|
213
|
+
help="Quiet sessions to analyse per run with no evidence behind them. Off "
|
|
214
|
+
"by default: it is the only route that spends inference with no evidence, "
|
|
215
|
+
"so it is asked for rather than assumed. Shares the --budget cap.",
|
|
216
|
+
)
|
|
217
|
+
@click.option(
|
|
218
|
+
"--cache/--no-cache",
|
|
219
|
+
default=True,
|
|
220
|
+
show_default=True,
|
|
221
|
+
help="Reuse a stored verdict for a session that has not changed. Holds rule "
|
|
222
|
+
"ids, grades and span identifiers only — never conversation (ADR-0011).",
|
|
223
|
+
)
|
|
224
|
+
@click.option(
|
|
225
|
+
"--project-map",
|
|
226
|
+
"project_map",
|
|
227
|
+
multiple=True,
|
|
228
|
+
metavar="OLD=NEW",
|
|
229
|
+
help="Where a project a session ran in lives now, for a directory that has "
|
|
230
|
+
"moved since. Applies to OLD and everything under it, only where OLD no "
|
|
231
|
+
"longer exists; repeatable, most specific wins. Every result is marked with "
|
|
232
|
+
"where it was mapped from.",
|
|
233
|
+
)
|
|
234
|
+
@click.option(
|
|
235
|
+
"--root",
|
|
236
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
237
|
+
default=None,
|
|
238
|
+
help="Read agent transcripts from here instead of the default location.",
|
|
239
|
+
)
|
|
240
|
+
def detect(
|
|
241
|
+
agent_kinds: tuple[str, ...],
|
|
242
|
+
since: str,
|
|
243
|
+
bom_paths: tuple[Path, ...],
|
|
244
|
+
output_format: str,
|
|
245
|
+
detail: bool,
|
|
246
|
+
escalate: bool,
|
|
247
|
+
budget: int,
|
|
248
|
+
sample_budget: int,
|
|
249
|
+
cache: bool,
|
|
250
|
+
project_map: tuple[str, ...],
|
|
251
|
+
root: Path | None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Find security and reliability findings in what agents did.
|
|
254
|
+
|
|
255
|
+
Four kinds of finding, under two families: a credential reaching an
|
|
256
|
+
outbound call and an injected instruction followed are security; a
|
|
257
|
+
vulnerable component actually reached is security with a vulnerability
|
|
258
|
+
behind it; a stalled loop and a hung call are reliability. The two
|
|
259
|
+
families carry their own severity ladders, because a stalled loop and a
|
|
260
|
+
leaked credential cannot share one (ADR-0010).
|
|
261
|
+
|
|
262
|
+
Two stages need no model or credential. The third sends flagged sessions to
|
|
263
|
+
the agent's own CLI: on by default and capped by --budget, with
|
|
264
|
+
--no-escalate to turn it off.
|
|
265
|
+
|
|
266
|
+
That third stage is the one that sends session content off this machine:
|
|
267
|
+
the agent's own CLI hands the prompts, arguments and results a rule needs
|
|
268
|
+
to the provider it is already authenticated against (ADR-0004 -- provider
|
|
269
|
+
affinity, so a transcript goes back to the vendor that produced it, and
|
|
270
|
+
there is no fallback to any other). --no-escalate leaves the two stages
|
|
271
|
+
that send nothing.
|
|
272
|
+
|
|
273
|
+
One network call happens before any of them regardless of --no-escalate:
|
|
274
|
+
correlation matches advisories by sending the package coordinates of the
|
|
275
|
+
components a session invoked to osv.dev. Coordinates only -- never a
|
|
276
|
+
prompt, an argument or a result.
|
|
277
|
+
"""
|
|
278
|
+
view = collect_sessions(list(agent_kinds) or None, _parse_since(since), root=root)
|
|
279
|
+
try:
|
|
280
|
+
acquired = acquire_correlated_view(
|
|
281
|
+
view, bom_paths=bom_paths, project_map=tuple(parse_mapping(m) for m in project_map)
|
|
282
|
+
)
|
|
283
|
+
except ValueError as error:
|
|
284
|
+
raise click.ClickException(str(error)) from error
|
|
285
|
+
|
|
286
|
+
result = run_detector(
|
|
287
|
+
acquired.view,
|
|
288
|
+
escalate=escalate,
|
|
289
|
+
budget=budget,
|
|
290
|
+
sample_budget=sample_budget,
|
|
291
|
+
# Only ever consulted when stage 3 runs: there is nothing to reuse
|
|
292
|
+
# otherwise, and opening a directory to discover that is waste.
|
|
293
|
+
cache=VerdictCache(default_directory()) if cache and escalate else None,
|
|
294
|
+
)
|
|
295
|
+
output = (
|
|
296
|
+
render_detections_json(result)
|
|
297
|
+
if output_format == "json"
|
|
298
|
+
else render_detections_text(result, detail)
|
|
299
|
+
)
|
|
300
|
+
click.echo(output, nl=not output.endswith("\n"))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Named, never discovered: an upstream release must not be able to enlarge this
|
|
304
|
+
# CLI. A missing name is a `KeyError` at import time, which names the command
|
|
305
|
+
# and the line, and lands at upgrade time.
|
|
306
|
+
for _name in PASSTHROUGH:
|
|
307
|
+
main.add_command(openaca_cli.commands[_name], _name)
|
|
308
|
+
|
|
309
|
+
# Native, registered the ordinary way and after the loop, so a name collision
|
|
310
|
+
# resolves in the native command's favour. The one recorded overlap is
|
|
311
|
+
# openaca's own `remote`, which is never mounted here.
|
|
312
|
+
main.add_command(remote_cmd, "remote")
|
|
313
|
+
main.add_command(sessions)
|
|
314
|
+
main.add_command(detect)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Correlation: the join between what an agent did and what it is built from."""
|