stacktrace-cli 0.2.1__py3-none-any.whl → 0.2.2__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.
@@ -1,3 +1,3 @@
1
1
  """The `stacktrace` command-line interface — detection and response for AI agents."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -176,6 +176,12 @@ def _banners(analysis: Analysis) -> list[str]:
176
176
  A blank column and a clean run render identically, which is the one
177
177
  confusion this page refuses: a reader who cannot tell "nothing happened"
178
178
  from "nothing could be read" has been told nothing.
179
+
180
+ Both banners are about a *reader* that came back with nothing. A kind with
181
+ no composition is not one of those and no longer appears: the calls it
182
+ accounts for are read, and the page shows what it can place them to.
183
+ `view.without_composition` is still reported by `stacktrace correlate`,
184
+ where the record rather than the console is the subject.
179
185
  """
180
186
  banners: list[str] = []
181
187
  for failure in analysis.run.collection_failures:
@@ -183,8 +189,6 @@ def _banners(analysis: Analysis) -> list[str]:
183
189
  anonymous = len(analysis.view.kind_anonymous)
184
190
  if anonymous:
185
191
  banners.append(f"{anonymous} session(s) skipped: no agent kind to attribute them to")
186
- for kind in analysis.view.without_composition:
187
- banners.append(f"{kind}: no composition, so every call reads as unresolved")
188
192
  return banners
189
193
 
190
194
 
@@ -145,16 +145,20 @@ function renderBanners(banners) {
145
145
 
146
146
  function renderCounts(state) {
147
147
  const summary = state.summary || {};
148
- /* Coverage is a fraction, never a bare percentage: 1 of 1 and 400 of 400 are
149
- * both "100%" and say very different things about how much this page knows.
150
- * And it is `null` until correlation has run, which is the first second or
151
- * so — said as "placing…" rather than shown as 0/0, which would read as
152
- * "nothing placed" instead of "not counted yet". */
153
- const coverage = summary.coverage;
148
+ /* Sessions and actions, and no coverage fraction. `6,867/7,055 placed` is
149
+ * the count of what this page leaves off, stated: the gap between the two
150
+ * numbers is exactly the unplaceable rows, so the header reinstated by
151
+ * arithmetic what the rows below had stopped saying. The snapshot still
152
+ * carries it — `stacktrace correlate` is where coverage is the subject.
153
+ *
154
+ * Actions are counted off the rows this page keeps rather than read from
155
+ * `summary.actions`, which is the server's total over every call in the
156
+ * window. Read straight through, the header claimed more actions than the
157
+ * feed below it could account for. */
158
+ const actions = eventsFrom(state.sessions || []).length;
154
159
  text(el("live-counts"),
155
160
  " — " + plural(summary.sessions || 0, "session") +
156
- " · " + plural(summary.actions || 0, "action") +
157
- " · " + (coverage ? coverage.resolved + "/" + coverage.total + " placed" : "placing…"));
161
+ " · " + plural(actions, "action"));
158
162
  }
159
163
 
160
164
  /* --- the demo's vocabulary ----------------------------------------------- */
@@ -209,7 +213,6 @@ const RULE_PLAIN = {
209
213
  "stacktrace-credential-egress": "A password or token was sent to an outside service.",
210
214
  "stacktrace-capability-crossing": "Private data was read, then sent outside.",
211
215
  "stacktrace-intent-drift": "It acted on something nobody asked about.",
212
- "stacktrace-unsanctioned-mcp-tool-use": "A tool ran that isn't in your installed set.",
213
216
  "stacktrace-guardrail-modification": "A session changed the agent's own rules or settings.",
214
217
  "stacktrace-injection-marker": "Injected-instruction markers were found in what the agent read.",
215
218
  "stacktrace-destructive-action": "A destructive action was taken (deleting or overwriting).",
@@ -227,20 +230,32 @@ const KIND_TAG = {
227
230
  subagent: ["sub-agent", "kt-sub"],
228
231
  mcp: ["mcp", "kt-conn"],
229
232
  command: ["command", "kt-cmd"],
230
- unresolved: ["⚠ unresolved", "kt-warn"],
231
233
  };
232
234
 
233
235
  /* Plural, because every one of these labels a count. */
234
236
  const KIND_LABEL = {
235
237
  mcp: "MCP servers", skill: "Skills", command: "Commands",
236
- subagent: "Sub-agents", tool: "Built-in tools", unresolved: "Unresolved",
238
+ subagent: "Sub-agents", tool: "Built-in tools",
237
239
  };
238
240
 
239
241
  /* Most consequential first, rather than by count: sorted by count the
240
242
  * built-ins win every window and push the MCP servers — the only category
241
243
  * describing a call that left the machine — under a number three orders of
242
244
  * magnitude larger. */
243
- const KIND_ORDER = ["mcp", "skill", "command", "subagent", "tool", "unresolved"];
245
+ const KIND_ORDER = ["mcp", "skill", "command", "subagent", "tool"];
246
+
247
+ /* The category the correlator gives a call it could not place. Every surface
248
+ * on this page drops these rows: an unplaceable call names no component, so
249
+ * the row could only say that something happened and nothing here claims it,
250
+ * which is not a thing a reader can act on. The count is still computed
251
+ * server-side and still reaches the detector — this is a decision about the
252
+ * page, not about the record. */
253
+ const UNPLACEABLE = "unresolved";
254
+
255
+ /* Whether the snapshot row is one of those. */
256
+ function unplaceable(row) {
257
+ return row.category === UNPLACEABLE;
258
+ }
244
259
  const TOP_PER_KIND = 3;
245
260
 
246
261
  const SEVERITY_WORD = { critical: "Serious", high: "High", medium: "Medium", low: "Low" };
@@ -294,10 +309,13 @@ function evParts(e) {
294
309
  provenance: "a link to an outside service",
295
310
  };
296
311
  }
312
+ /* A category this build has never heard of. Named as itself rather than
313
+ * described: anything else this function said about it would be invented,
314
+ * and the one category it used to describe no longer reaches the page. */
297
315
  return {
298
316
  name: e.name,
299
- action: "used a tool nothing here claims",
300
- provenance: "no component on this machine declares it",
317
+ action: "used " + (e.tool || "a tool"),
318
+ provenance: e.kind ? "category " + e.kind : "",
301
319
  };
302
320
  }
303
321
 
@@ -321,6 +339,7 @@ function eventsFrom(sessions) {
321
339
  const activity = session.activity || [];
322
340
  for (let index = 0; index < activity.length; index += 1) {
323
341
  const row = activity[index];
342
+ if (unplaceable(row)) continue;
324
343
  events.push({
325
344
  id: row.span,
326
345
  kind: row.category,
@@ -336,11 +355,6 @@ function eventsFrom(sessions) {
336
355
  * the skills a reader had just run sat under MCP calls from half a day
337
356
  * earlier. Named for what it is, because it is not the start. */
338
357
  at: session.last_active || session.started_at,
339
- /* Both worth a reader's attention; only the first is a claim about
340
- * attribution. Correlation keeps a refused call's resolution, so the
341
- * row beside the label names the component it would have reached. */
342
- unplaced: row.category === "unresolved",
343
- refused: row.status === "denied",
344
358
  order: index,
345
359
  });
346
360
  }
@@ -356,7 +370,8 @@ function compositionOf(sessions) {
356
370
  const seen = new Set();
357
371
  for (const session of sessions) {
358
372
  for (const row of session.activity || []) {
359
- const type = row.component_type || (row.category === "unresolved" ? null : row.category);
373
+ if (unplaceable(row)) continue;
374
+ const type = row.component_type || row.category;
360
375
  const key = type + "\n" + (row.component_identity || row.component_name || row.tool_name);
361
376
  if (!type || seen.has(key)) continue;
362
377
  seen.add(key);
@@ -372,6 +387,7 @@ function activitySummaryOf(sessions) {
372
387
  const named = {};
373
388
  for (const session of sessions) {
374
389
  for (const row of session.activity || []) {
390
+ if (unplaceable(row)) continue;
375
391
  byKind[row.category] = (byKind[row.category] || 0) + 1;
376
392
  if (!named[row.category]) named[row.category] = new Map();
377
393
  const label = entryLabel(row);
@@ -531,9 +547,9 @@ let revealTimer = null;
531
547
  * Routine built-ins (Bash, Read, Edit) are the constant background of every
532
548
  * session: 5,795 of one window's 7,065 calls. They collapse **per project**
533
549
  * into one counting row for the agent. Notable events — skills, MCP servers,
534
- * sub-agents, commands, and anything unresolved — are rare and important, so
535
- * each gets its own row, keyed by span and never coalesced: two runs of
536
- * `superpowers:brainstorming` are two things that happened.
550
+ * sub-agents and commands — are rare and important, so each gets its own row,
551
+ * keyed by span and never coalesced: two runs of `superpowers:brainstorming`
552
+ * are two things that happened.
537
553
  *
538
554
  * Grouping by tool name instead gave `Bash ×38 / Read ×16 / Bash ×3 / Edit
539
555
  * ×20` per session, with every skill and connector buried under it. */
@@ -609,14 +625,6 @@ function countAgain(entry, e) {
609
625
  entry.timeEl.textContent = fmtRange(entry.first, entry.last);
610
626
  }
611
627
  }
612
- /* A refusal is worth seeing and was not worth a row of its own: `Bash ×3
613
- * denied` beside `Bash ×38` was half the noise. It rides the row instead. */
614
- if (e.refused) {
615
- entry.refused += 1;
616
- entry.node.classList.add("warn");
617
- entry.warnEl.hidden = false;
618
- entry.warnEl.textContent = "⚠ " + plural(entry.refused, "refusal");
619
- }
620
628
  /* Animated where it sits, never hoisted. Hoisting the most recently counted
621
629
  * row would rank a busy week-old session above a quiet one from this
622
630
  * morning: during a backfill, "counted just now" is a fact about the drain
@@ -656,14 +664,19 @@ function place(entry) {
656
664
 
657
665
  function newRow(e, signature) {
658
666
  const parts = evParts(e);
659
- const kind = KIND_TAG[e.kind] || KIND_TAG.unresolved;
667
+ /* A category this build has never heard of takes its own name and the
668
+ * neutral tag: nothing on this page knows enough about it to warn. */
669
+ const kind = KIND_TAG[e.kind] || [e.kind || "", "kt-tool"];
660
670
  /* The placeholder goes as soon as there is a real row — otherwise it rides
661
671
  * the list and counts against the trim. */
662
672
  if (!feedOrder.length) activityFeed.innerHTML = "";
663
673
 
674
+ /* No row this feed builds is reddened. The two sources of it were a call
675
+ * the correlator could not place — which no longer reaches the feed — and a
676
+ * refusal, which `outcome.py` is explicit is a guardrail working as
677
+ * intended: painting it red reports the permission prompt as the incident. */
664
678
  const node = document.createElement("div");
665
679
  node.className = "ev fresh";
666
- if (e.unplaced || e.refused) node.classList.add("warn");
667
680
 
668
681
  const countEl = element("span", "evcount", "");
669
682
  countEl.hidden = true;
@@ -674,16 +687,6 @@ function newRow(e, signature) {
674
687
  first.appendChild(element("span", "evact", parts.action));
675
688
  first.appendChild(countEl);
676
689
 
677
- const warnEl = element("span", "ev-warn", "");
678
- warnEl.hidden = true;
679
- if (e.unplaced) {
680
- warnEl.hidden = false;
681
- warnEl.textContent = "unplaced";
682
- } else if (e.refused) {
683
- warnEl.hidden = false;
684
- warnEl.textContent = e.kind === "tool" ? "⚠ 1 refusal" : "refused";
685
- }
686
-
687
690
  const lastEl = element("span", "evlast", "");
688
691
  const timeEl = element("span", "ev-time", fmtAgo(e.at));
689
692
 
@@ -692,15 +695,14 @@ function newRow(e, signature) {
692
695
  second.appendChild(element("span", "ev-prov", parts.provenance));
693
696
  second.appendChild(lastEl);
694
697
  second.appendChild(timeEl);
695
- second.appendChild(warnEl);
696
698
 
697
699
  node.appendChild(first);
698
700
  node.appendChild(second);
699
701
 
700
702
  const started = Date.parse(e.at);
701
703
  const entry = {
702
- node: node, countEl: countEl, warnEl: warnEl, lastEl: lastEl, timeEl: timeEl,
703
- count: 1, refused: e.refused ? 1 : 0, key: keyOf(e), signature: signature,
704
+ node: node, countEl: countEl, lastEl: lastEl, timeEl: timeEl,
705
+ count: 1, key: keyOf(e), signature: signature,
704
706
  first: Number.isNaN(started) ? Infinity : started,
705
707
  last: Number.isNaN(started) ? -Infinity : started,
706
708
  };
@@ -74,8 +74,6 @@
74
74
 
75
75
  <div class="legend" id="live-legend">
76
76
  <span><b class="lg-conn">MCP</b> = a link to an outside service</span>
77
- <span><b class="lg-ok">&#10003;</b> resolved to something you installed</span>
78
- <span><b class="lg-warn">&#9888;</b> unresolved &mdash; nothing on this machine claims it</span>
79
77
  </div>
80
78
 
81
79
  <div class="logs">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: stacktrace-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI for Stacktrace — Detection and Response platform for AI Agents.
5
5
  Project-URL: Homepage, https://stacktrace.ai
6
6
  Author-email: "Stacktrace AI, Inc" <founders@stacktrace.ai>
@@ -1,4 +1,4 @@
1
- stacktrace_cli/__init__.py,sha256=wEqCraAEngRIhaLrRbqpgR5EHnxQ-vG2K3YDaP-Lrgc,111
1
+ stacktrace_cli/__init__.py,sha256=7-HhS3y6stqt0b0tldy0IhMr_bOjUk_HMTHwNj4LgyI,111
2
2
  stacktrace_cli/__main__.py,sha256=F_tqj3PRzeBtY-uvBs923H8oc-s9bebhIh6UnAUwNYc,448
3
3
  stacktrace_cli/analysis.py,sha256=PECyhd_398fDVjg3K78HVzB4xHYS7sfLNXpASRXNRsY,15303
4
4
  stacktrace_cli/cli.py,sha256=tJjGtGlaI6OB4bdlgX3q63VffZxRIjwiCwH54kzxRAo,17032
@@ -32,13 +32,13 @@ stacktrace_cli/detector/prompts/v1/stacktrace-injected-instruction-followed.md,s
32
32
  stacktrace_cli/detector/prompts/v1/stacktrace-intent-drift.md,sha256=1dI4bUIonwm6t2x32GnV-yGNfb1jGibeBo6Wli62WZI,455
33
33
  stacktrace_cli/monitor/__init__.py,sha256=akhxzlSGyvbG-3WyC-5Ch5nuYP1E6xga8rzkJ6Tn-AU,469
34
34
  stacktrace_cli/monitor/escalate.py,sha256=tpkp88xKyHX0BRVFow8b9_8YswduAAofidMiZBpRix4,7398
35
- stacktrace_cli/monitor/render.py,sha256=0Wb3Q6y6VRo6GCkULuyqvC7huzCdOejyjeMInxz1DTg,9835
35
+ stacktrace_cli/monitor/render.py,sha256=oR3Sr2Tlk2_NtpVmJfLnU_IH-bkXIs7p4a27OZgn4mY,10064
36
36
  stacktrace_cli/monitor/server.py,sha256=ZTKUy3Ct-7rfsErN3Cpr5PaaZ5SX_OMw-ExFe6FKL9w,19683
37
37
  stacktrace_cli/monitor/state.py,sha256=UVTLpAecypsLTBs5IdWlY55dyJTf-2jI8lDAWoH9PP4,4157
38
38
  stacktrace_cli/monitor/verdicts.py,sha256=V6IK2C9eVxZ2RXjWkfDdmfvN8f-Ol67U-N5c1kXx8rQ,2275
39
39
  stacktrace_cli/monitor/watch.py,sha256=LrbS6RnZg322tbESQ4u2rTVxhkxsNI5UwdhpTEO3GgE,15546
40
- stacktrace_cli/monitor/site/app.js,sha256=zk_dVPLp174NTaHcBi0SEQGkGxl4fiOIf4Y77MHnKKc,43668
41
- stacktrace_cli/monitor/site/index.html,sha256=7zOdxjo6GkfaG56kwvP8Zm66ITyO-Wadn6kO99qVpDM,4169
40
+ stacktrace_cli/monitor/site/app.js,sha256=CM28lPaxuy-oourHb85jw35NDhqnFpYxc0bKgQsLrX0,43898
41
+ stacktrace_cli/monitor/site/index.html,sha256=OhbyY9Dnvec90IMc9a8iGBZdgfk38aNmbyhwhrrLXYM,3985
42
42
  stacktrace_cli/monitor/site/styles.css,sha256=ol_sSrn2Jwk5xAtz9Avq2itUp9r1Z-nv6sf7KSwNSlU,35603
43
43
  stacktrace_cli/monitor/site/fonts/OFL.txt,sha256=3eGN4fg4dKG6QBHOpLJobIeUAs3eQnmZBiykH5b-TGY,9975
44
44
  stacktrace_cli/monitor/site/fonts/dm-mono-400-latin.woff2,sha256=_XUh81MaXM_GVbJcTyLphx3z7BQa15uyf94g0N80e20,8688
@@ -61,7 +61,7 @@ stacktrace_cli/sessions/access.py,sha256=mpKMdRufPGTZNJCVPa2MoKWdM8eOYzaYMDsGqob
61
61
  stacktrace_cli/sessions/outcome.py,sha256=IQUrOoigPAFHnoA26LlOjSSB8hnSQUIJ8E5tQ0LdUJk,2632
62
62
  stacktrace_cli/sessions/protocols.py,sha256=rvQsVd77AekNGhp73masMa61-DbG62QF8Y6hDSwQ9bU,6630
63
63
  stacktrace_cli/sessions/render.py,sha256=SCu9Fx-7hWQWOsy3O7zbYzJqCNomaMp_19V7NQTkSm0,14001
64
- stacktrace_cli-0.2.1.dist-info/METADATA,sha256=vBb-yx_T0sV9KWDkoqQo-NDg7uwQBqC8yMZo32WIoyo,5459
65
- stacktrace_cli-0.2.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
66
- stacktrace_cli-0.2.1.dist-info/entry_points.txt,sha256=OYDmb2CtEjd8TV78zxiGB1XVrLrC6vvayAPXa79_tJ0,60
67
- stacktrace_cli-0.2.1.dist-info/RECORD,,
64
+ stacktrace_cli-0.2.2.dist-info/METADATA,sha256=uAgSCLSXgp-IdFJ8L3jwRT9Z_GEjNQeFv7608lCBcQY,5459
65
+ stacktrace_cli-0.2.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
66
+ stacktrace_cli-0.2.2.dist-info/entry_points.txt,sha256=OYDmb2CtEjd8TV78zxiGB1XVrLrC6vvayAPXa79_tJ0,60
67
+ stacktrace_cli-0.2.2.dist-info/RECORD,,