costwright 0.2.2__tar.gz → 0.2.3__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.
- {costwright-0.2.2 → costwright-0.2.3}/PKG-INFO +1 -1
- {costwright-0.2.2 → costwright-0.2.3}/pyproject.toml +1 -1
- costwright-0.2.3/src/costwright/__init__.py +1 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/caps.py +34 -21
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/cli.py +5 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/extract.py +16 -2
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/fusion.py +14 -2
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright.egg-info/PKG-INFO +1 -1
- costwright-0.2.2/src/costwright/__init__.py +0 -1
- {costwright-0.2.2 → costwright-0.2.3}/LICENSE +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/README.md +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/setup.cfg +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/mapper.py +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/pack.py +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/report.py +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright/subgraph.py +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright.egg-info/SOURCES.txt +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright.egg-info/dependency_links.txt +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright.egg-info/entry_points.txt +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/src/costwright.egg-info/top_level.txt +0 -0
- {costwright-0.2.2 → costwright-0.2.3}/tests/test_harness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: costwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Static budget certificates for LLM-agent workflows (LangGraph / CrewAI / OpenAI Agents SDK). Backed by a machine-checked (Lean 4) cost-soundness theorem.
|
|
5
5
|
Author: Hernán Inverso
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "costwright"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Static budget certificates for LLM-agent workflows (LangGraph / CrewAI / OpenAI Agents SDK). Backed by a machine-checked (Lean 4) cost-soundness theorem."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -76,6 +76,11 @@ def scan_file(path: Path):
|
|
|
76
76
|
model_val = next((k.value.value for k in node.keywords
|
|
77
77
|
if k.arg == "model" and isinstance(k.value, ast.Constant)
|
|
78
78
|
and isinstance(k.value.value, str)), "")
|
|
79
|
+
# the model can also be the FIRST POSITIONAL arg — `ChatOpenAI("gpt-5")` (codex/Cursor r76); otherwise
|
|
80
|
+
# a reasoning model passed positionally would escape the reasoning detection below.
|
|
81
|
+
if not model_val and node.args and isinstance(node.args[0], ast.Constant) \
|
|
82
|
+
and isinstance(node.args[0].value, str):
|
|
83
|
+
model_val = node.args[0].value
|
|
79
84
|
reasoning = any(model_val.startswith(p) for p in
|
|
80
85
|
("o1", "o3", "o4", "gpt-5")) if model_val else False
|
|
81
86
|
# SOLO Chat-API constructors (audit-3 R2 gpt-5.5): el constructor `OpenAI` es
|
|
@@ -134,30 +139,38 @@ def scan_file(path: Path):
|
|
|
134
139
|
|
|
135
140
|
|
|
136
141
|
def make_patch(path: Path, src: str, findings, cap_value: int) -> str:
|
|
137
|
-
"""Unified diff que agrega `kwarg=cap_value`
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
"""Unified diff que agrega `kwarg=cap_value` como ÚLTIMO argumento de cada constructor sin cap.
|
|
143
|
+
Inserción basada en AST (robusta a args POSICIONALES, strings con paréntesis, y kwargs previos): el kwarg
|
|
144
|
+
va antes del `)` de cierre del call, NUNCA tras el `(` (eso produciría `Ctor(kwarg=…, "positional")` =
|
|
145
|
+
SyntaxError — codex/Cursor r76). NUNCA escribe el archivo — solo el diff (council 002 P0-2)."""
|
|
146
|
+
try:
|
|
147
|
+
tree = ast.parse(src)
|
|
148
|
+
except SyntaxError:
|
|
149
|
+
return ""
|
|
150
|
+
# map (lineno, constructor) → list of Call nodes, to insert at the exact end of the right call
|
|
151
|
+
by_key = {}
|
|
152
|
+
for node in ast.walk(tree):
|
|
153
|
+
if isinstance(node, ast.Call):
|
|
154
|
+
by_key.setdefault((node.lineno, call_name(node)), []).append(node)
|
|
140
155
|
lines = src.splitlines(keepends=True)
|
|
141
156
|
new_lines = list(lines)
|
|
142
|
-
#
|
|
143
|
-
for f in
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
edits = [] # (line_index, col, text) — applied right-to-left so columns don't shift
|
|
158
|
+
for f in (f for f in findings if f["kind"] == "missing"):
|
|
159
|
+
cands = by_key.get((f["line"], f["constructor"]), [])
|
|
160
|
+
if len(cands) != 1:
|
|
161
|
+
continue # 0 or >1 matching calls on the line → ambiguous, skip (the finding is still reported)
|
|
162
|
+
call = cands[0]
|
|
163
|
+
if call.end_lineno != call.lineno or call.end_col_offset is None:
|
|
164
|
+
continue # multi-line call → skip (conservative)
|
|
165
|
+
i = call.lineno - 1
|
|
166
|
+
close = call.end_col_offset - 1 # column of the closing ')'
|
|
167
|
+
had_args = bool(call.args) or bool(call.keywords)
|
|
168
|
+
sep = ", " if had_args else ""
|
|
169
|
+
edits.append((i, close, f"{sep}{f['suggest_kwarg']}={cap_value}"))
|
|
170
|
+
# apply right-to-left (highest column on a line first) so earlier insertions don't shift later columns
|
|
171
|
+
for i, col, text in sorted(edits, key=lambda e: (e[0], -e[1])):
|
|
148
172
|
line = new_lines[i]
|
|
149
|
-
|
|
150
|
-
# audit-3 (gemini P0): si hay >1 ocurrencia del constructor en la línea, NO parchear
|
|
151
|
-
# (la inserción textual no sabe cuál es cuál) — conservador, el hallazgo igual se reporta
|
|
152
|
-
if line.count(ctor + "(") != 1:
|
|
153
|
-
continue
|
|
154
|
-
idx = line.find(ctor + "(")
|
|
155
|
-
if idx < 0:
|
|
156
|
-
continue # constructor multilínea: skip (conservador)
|
|
157
|
-
insert_at = idx + len(ctor) + 1
|
|
158
|
-
rest = line[insert_at:]
|
|
159
|
-
sep = "" if rest.lstrip().startswith(")") else ", "
|
|
160
|
-
new_lines[i] = line[:insert_at] + f"{f['suggest_kwarg']}={cap_value}{sep}" + rest
|
|
173
|
+
new_lines[i] = line[:col] + text + line[col:]
|
|
161
174
|
if new_lines == lines:
|
|
162
175
|
return ""
|
|
163
176
|
rel = str(path)
|
|
@@ -143,6 +143,11 @@ def cmd_caps(args) -> int:
|
|
|
143
143
|
print(f"\n {total} finding(s) in {len(per_file)} file(s) "
|
|
144
144
|
f"({scanned} scanned). Use --patch to emit a unified diff.")
|
|
145
145
|
if args.patch:
|
|
146
|
+
if args.cap < 1:
|
|
147
|
+
# a cap of 0/negative is not an effective token bound — the patch would insert an inert kwarg
|
|
148
|
+
# that costwright itself flags `ineffective` (codex r75). Refuse instead of suggesting it.
|
|
149
|
+
print(f"costwright: --cap must be a positive integer (got {args.cap})", file=sys.stderr)
|
|
150
|
+
return 2
|
|
146
151
|
chunks = []
|
|
147
152
|
for p, (fs, src) in sorted(per_file.items()):
|
|
148
153
|
d = caps_mod.make_patch(p.relative_to(root), src, fs, args.cap)
|
|
@@ -227,8 +227,16 @@ class Extractor(ast.NodeVisitor):
|
|
|
227
227
|
# CrewAI Agent sin max_iter → default 20 (lo decide el mapper por-kind)
|
|
228
228
|
elif last == "Crew":
|
|
229
229
|
proc = next((k for k in n.keywords if k.arg == "process"), None)
|
|
230
|
-
if proc is not None
|
|
231
|
-
|
|
230
|
+
if proc is not None:
|
|
231
|
+
# a hierarchical Crew runs a MANAGER that re-delegates (an unbounded loop) → fail closed. The
|
|
232
|
+
# only SAFE value is a confirmed-sequential LITERAL (`Process.sequential` / "sequential"). A
|
|
233
|
+
# hierarchical literal, a VARIABLE (`mode = Process.hierarchical; process=mode` — codex r75), or
|
|
234
|
+
# any computed expression could be the manager → conservatively flag hierarchical-manager.
|
|
235
|
+
dump = ast.dump(proc.value)
|
|
236
|
+
confirmed_sequential = (isinstance(proc.value, (ast.Attribute, ast.Constant))
|
|
237
|
+
and "sequential" in dump and "hierarchical" not in dump)
|
|
238
|
+
if not confirmed_sequential:
|
|
239
|
+
s.features.append({"feature": "hierarchical-manager", "line": n.lineno})
|
|
232
240
|
|
|
233
241
|
# caps de tokens en cualquier call (constructores de modelos, llamadas)
|
|
234
242
|
for k in n.keywords:
|
|
@@ -243,6 +251,12 @@ class Extractor(ast.NodeVisitor):
|
|
|
243
251
|
|
|
244
252
|
def _scan_invoke(s, n):
|
|
245
253
|
"""Busca recursion_limit / max_turns en el config del call-site (D2)."""
|
|
254
|
+
# a **kwargs spread on an invoke/run call is OPAQUE — it could carry a max_turns / recursion_limit that
|
|
255
|
+
# DISABLES the cap (e.g. `Runner.run(a, **{"max_turns": None})`, `app.invoke({}, **opts)`) and the bound
|
|
256
|
+
# would be unrecoverable → record an UNRESOLVED bound so the mapper fails closed (codex/Cursor r79).
|
|
257
|
+
if any(k.arg is None for k in n.keywords):
|
|
258
|
+
s.bounds.append({"param": "invoke-kwargs-spread", "value": None,
|
|
259
|
+
"source": "explicit", "line": n.lineno})
|
|
246
260
|
for k in n.keywords:
|
|
247
261
|
if k.arg == "max_turns":
|
|
248
262
|
# distinguir None LITERAL (desactivación deliberada) de expresión no-constante
|
|
@@ -80,6 +80,8 @@ _SLA_MODES = {"strict", "balanced"}
|
|
|
80
80
|
# it is a reported analysis the signed bundle BINDS (tamper-evidence) and whose ARITHMETIC fusion
|
|
81
81
|
# re-checks in pure stdlib — but whose operational ASSUMPTIONS fusion canNOT verify (self-asserted).
|
|
82
82
|
_INTERF_KIND = "tv-coupling-bound"
|
|
83
|
+
_CHANNEL_COVERED = "budget-cap-distribution-shift (channel 1 of N; N unknown)"
|
|
84
|
+
_SOURCE_ESTIMATOR = "eleata-verify.epsilon.interference_risk_bound"
|
|
83
85
|
_ASSURANCE_LEVELS = {"self_asserted", "evidence_attached", "independently_reviewed"}
|
|
84
86
|
_ASSUMPTIONS = {"A", "C", "D"} # the operational assumptions the (ii) bound needs
|
|
85
87
|
# status is NEVER "bounded" (council P0-1: no word that reads as a guarantee). Derived by fusion.
|
|
@@ -423,8 +425,8 @@ def conditional_analysis_from_epsilon(epsilon_bound: dict, *, assumptions_attest
|
|
|
423
425
|
_bound_auth = _inflate_alpha(float(_ab), _eps_auth, float(_cu))
|
|
424
426
|
block = {
|
|
425
427
|
"kind": _INTERF_KIND,
|
|
426
|
-
"channel_covered":
|
|
427
|
-
"source_estimator":
|
|
428
|
+
"channel_covered": _CHANNEL_COVERED,
|
|
429
|
+
"source_estimator": _SOURCE_ESTIMATOR,
|
|
428
430
|
"verify_version": str(verify_version),
|
|
429
431
|
"note": INTERF_NOTE,
|
|
430
432
|
"channel1_conditional_risk_upper": _bound_auth, # RECOMPUTED, not the caller's alpha_effective
|
|
@@ -562,6 +564,16 @@ def _validate_conditional_analyses(ca: dict, risk_block: dict) -> dict:
|
|
|
562
564
|
out["assumptions_complete"] = assumptions_complete
|
|
563
565
|
out["bound_verification"] = bound_verification
|
|
564
566
|
out["open_channels_non_exhaustive"] = True # forced — the list is non-exhaustive by construction
|
|
567
|
+
# FORCE the honesty/provenance fields to costwright's own constants — the caller cannot inject a
|
|
568
|
+
# `disclaimer="GUARANTEED SAFE"`, a reassuring `note`, a shrunk `open_channels=["none"]`, or a misleading
|
|
569
|
+
# `channel_covered`/`source_estimator` into the signed bundle (codex r78). Only measured PRIMITIVES
|
|
570
|
+
# (k, m, δ_eps, α, c, attestations) come from the caller; every honesty string is costwright's.
|
|
571
|
+
out["kind"] = _INTERF_KIND
|
|
572
|
+
out["channel_covered"] = _CHANNEL_COVERED
|
|
573
|
+
out["source_estimator"] = _SOURCE_ESTIMATOR
|
|
574
|
+
out["note"] = INTERF_NOTE
|
|
575
|
+
out["open_channels"] = list(_OPEN_CHANNELS)
|
|
576
|
+
out["disclaimer"] = NON_INTERFERENCE
|
|
565
577
|
return {"channel1_budget_cap_risk": out}
|
|
566
578
|
|
|
567
579
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: costwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Static budget certificates for LLM-agent workflows (LangGraph / CrewAI / OpenAI Agents SDK). Backed by a machine-checked (Lean 4) cost-soundness theorem.
|
|
5
5
|
Author: Hernán Inverso
|
|
6
6
|
License: Apache-2.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|