deepbom 1.103.1 → 1.104.0
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.
- package/README.md +6 -6
- package/bin/capture-activation-evidence.py +195 -0
- package/bin/deepbom.mjs +111 -105
- package/package.json +1 -1
- package/pkg/release-manifest.json +4 -4
- package/pkg/tflite_wasm_audit_bg.wasm +0 -0
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@ For a local Codex or Claude Code project, preview and install the bundled Agent
|
|
|
7
7
|
Skill without operating an analysis server:
|
|
8
8
|
|
|
9
9
|
```console
|
|
10
|
-
npx -y deepbom@1.
|
|
11
|
-
npx -y deepbom@1.
|
|
12
|
-
npx -y deepbom@1.
|
|
13
|
-
npx -y deepbom@1.
|
|
10
|
+
npx -y deepbom@1.104.0 integrate codex
|
|
11
|
+
npx -y deepbom@1.104.0 integrate codex --apply
|
|
12
|
+
npx -y deepbom@1.104.0 integrate claude-code
|
|
13
|
+
npx -y deepbom@1.104.0 integrate claude-code --apply
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
```console
|
|
@@ -28,10 +28,10 @@ npx deepbom explore model.tflite --target-profile target-profile.json
|
|
|
28
28
|
npx deepbom audit model.pte --executorch-build deepbom.executorch-build.json --compact
|
|
29
29
|
npx deepbom capabilities --format agent-json
|
|
30
30
|
npx deepbom capabilities --format agent-text
|
|
31
|
-
npx -y deepbom@1.
|
|
31
|
+
npx -y deepbom@1.104.0 mcp
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Claude Desktop can install the version-matched `deepbom-1.
|
|
34
|
+
Claude Desktop can install the version-matched `deepbom-1.104.0.mcpb` asset
|
|
35
35
|
from the corresponding GitHub Release as a local desktop extension.
|
|
36
36
|
|
|
37
37
|
ChatGPT developer-mode users can connect `https://deepbom.org/mcp` for one
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explicit local ONNX/LiteRT execution capture. Never imported by static audit."""
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime
|
|
5
|
+
import hashlib
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sha(data):
|
|
15
|
+
return hashlib.sha256(data).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
20
|
+
p.add_argument("model", type=Path)
|
|
21
|
+
p.add_argument("--model-ir", type=Path, required=True)
|
|
22
|
+
p.add_argument("--output", type=Path, required=True)
|
|
23
|
+
p.add_argument("--allow-execution", action="store_true")
|
|
24
|
+
group = p.add_mutually_exclusive_group(required=True)
|
|
25
|
+
group.add_argument("--inputs-npz", type=Path)
|
|
26
|
+
group.add_argument("--probe", choices=["ones", "zeros", "identity"])
|
|
27
|
+
p.add_argument("--max-values", type=int, default=1_000_000)
|
|
28
|
+
p.add_argument("--outputs-only", action="store_true")
|
|
29
|
+
p.add_argument("--entry-region", help="Exact Model IR entry region ID for a model with multiple entries")
|
|
30
|
+
args = p.parse_args()
|
|
31
|
+
if not args.allow_execution:
|
|
32
|
+
p.error("Execution requires --allow-execution. Static audit never invokes this collector.")
|
|
33
|
+
if args.output.exists():
|
|
34
|
+
p.error("Output already exists; choose a new capture path.")
|
|
35
|
+
if args.max_values < 1 or args.max_values > 1_000_000:
|
|
36
|
+
p.error("--max-values must be between 1 and 1000000.")
|
|
37
|
+
import numpy as np
|
|
38
|
+
model_ir = json.loads(args.model_ir.read_text())
|
|
39
|
+
if model_ir.get("schema") == "deepbom.analysis_selection.v1":
|
|
40
|
+
model_ir = model_ir["sections"]["model_ir"]
|
|
41
|
+
raw = args.model.read_bytes()
|
|
42
|
+
if sha(raw) != model_ir["artifact"]["sha256"]:
|
|
43
|
+
raise ValueError("Model bytes do not match Model IR.")
|
|
44
|
+
source = {"model_ir_sha256": model_ir["model_ir_sha256"], "artifact_sha256": sha(raw), "artifact_set_sha256": model_ir["artifact_set"]["artifact_set_sha256"]}
|
|
45
|
+
values = model_ir["program"]["values"]
|
|
46
|
+
entries = [entry for program in model_ir["program"]["programs"] for entry in program["entry_region_refs"]]
|
|
47
|
+
if len(entries) != 1 and not args.entry_region:
|
|
48
|
+
raise ValueError("Multiple entry regions require --entry-region.")
|
|
49
|
+
entry = args.entry_region or entries[0]
|
|
50
|
+
if entry not in entries:
|
|
51
|
+
raise ValueError("Unknown entry region.")
|
|
52
|
+
expected_entry = {"onnx": "region:scope:onnx:main_graph", "tflite": "region:scope:tflite:subgraph:0"}.get(model_ir["artifact"]["format"])
|
|
53
|
+
if entry != expected_entry:
|
|
54
|
+
raise ValueError("This collector supports the primary ONNX graph or TFLite subgraph only.")
|
|
55
|
+
inputs = [v for v in values if "graph_input" in v["roles"] and v["region_ref"] == entry and not v["storage_refs"]]
|
|
56
|
+
eligible = [v for v in values if not ("graph_input" in v["roles"] and v["region_ref"] == entry) and not v["storage_refs"]]
|
|
57
|
+
requested = [v for v in eligible if not args.outputs_only or "graph_output" in v["roles"]]
|
|
58
|
+
dtype_map = {"FLOAT32": np.float32, "FLOAT16": np.float16, "FLOAT64": np.float64, "INT64": np.int64, "INT32": np.int32, "INT16": np.int16, "INT8": np.int8, "UINT64": np.uint64, "UINT32": np.uint32, "UINT16": np.uint16, "UINT8": np.uint8, "BOOL": np.bool_}
|
|
59
|
+
input_bytes = args.inputs_npz.read_bytes() if args.inputs_npz else None
|
|
60
|
+
npz = np.load(io.BytesIO(input_bytes), allow_pickle=False) if input_bytes is not None else None
|
|
61
|
+
feeds = {}
|
|
62
|
+
for item in inputs:
|
|
63
|
+
if item["name"] in feeds:
|
|
64
|
+
raise ValueError("Input names are ambiguous across serialized regions.")
|
|
65
|
+
dtype = dtype_map[item["dtype"]]
|
|
66
|
+
if npz is not None:
|
|
67
|
+
array = npz[item["name"]]
|
|
68
|
+
if array.dtype != dtype:
|
|
69
|
+
raise ValueError(f"Input {item['name']} dtype mismatch; implicit casts are disabled.")
|
|
70
|
+
else:
|
|
71
|
+
shape = item["shape"]
|
|
72
|
+
if any(not isinstance(d, int) or d < 0 for d in shape):
|
|
73
|
+
raise ValueError("Dynamic inputs require --inputs-npz with explicit dimensions.")
|
|
74
|
+
if math.prod(shape) > args.max_values:
|
|
75
|
+
raise ValueError("Synthetic input exceeds value budget.")
|
|
76
|
+
if args.probe == "identity":
|
|
77
|
+
if len(shape) != 2 or shape[0] != shape[1]:
|
|
78
|
+
raise ValueError("Identity probe requires a square rank-2 input.")
|
|
79
|
+
array = np.eye(shape[0], dtype=dtype)
|
|
80
|
+
else:
|
|
81
|
+
array = np.full(shape, int(args.probe == "ones"), dtype=dtype)
|
|
82
|
+
feeds[item["name"]] = array
|
|
83
|
+
if npz is not None:
|
|
84
|
+
if set(npz.files) != set(feeds):
|
|
85
|
+
raise ValueError("NPZ keys must exactly match serialized graph inputs.")
|
|
86
|
+
npz.close()
|
|
87
|
+
consumed = sum(array.size for array in feeds.values())
|
|
88
|
+
if consumed > args.max_values:
|
|
89
|
+
raise ValueError("Inputs exceed value budget.")
|
|
90
|
+
missing, selected = [], []
|
|
91
|
+
for item in requested:
|
|
92
|
+
shape = item["shape"]
|
|
93
|
+
expected = math.prod(shape) if all(isinstance(d, int) and d >= 0 for d in shape) else None
|
|
94
|
+
if item["dtype"] not in dtype_map:
|
|
95
|
+
missing.append({"value_ref": item["id"], "reason": "collector_dtype_unsupported"})
|
|
96
|
+
elif expected is None or consumed + expected > args.max_values:
|
|
97
|
+
missing.append({"value_ref": item["id"], "reason": "shape_or_capture_budget_unavailable"})
|
|
98
|
+
else:
|
|
99
|
+
selected.append(item)
|
|
100
|
+
consumed += expected
|
|
101
|
+
captures = []
|
|
102
|
+
def encode(item, array):
|
|
103
|
+
if str(array.dtype) not in [str(np.dtype(dtype_map[item["dtype"]]))]:
|
|
104
|
+
raise ValueError("Runtime capture dtype differs from Model IR.")
|
|
105
|
+
if array.size > args.max_values:
|
|
106
|
+
raise ValueError("Runtime tensor exceeds capture budget.")
|
|
107
|
+
numeric = []
|
|
108
|
+
for v in array.reshape(-1):
|
|
109
|
+
if array.dtype.kind in "iu": numeric.append(str(int(v)) if array.dtype.itemsize == 8 else int(v))
|
|
110
|
+
elif array.dtype.kind == "b": numeric.append(int(v))
|
|
111
|
+
else:
|
|
112
|
+
n = float(v)
|
|
113
|
+
numeric.append("NaN" if math.isnan(n) else "+Infinity" if n == math.inf else "-Infinity" if n == -math.inf else "-0" if n == 0 and math.copysign(1, n) < 0 else n)
|
|
114
|
+
return {"value_ref": item["id"], "native_locator": item["name"], "dtype": item["dtype"], "shape": list(array.shape), "values": numeric}
|
|
115
|
+
started = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
116
|
+
instrumented_hash = None
|
|
117
|
+
configuration = {"collector_version": "1.0.0", "max_values": args.max_values, "outputs_only": args.outputs_only, "probe": args.probe, "threads": 1, "numpy_version": np.__version__, "python_version": sys.version.split()[0], "inputs_npz_sha256": sha(input_bytes) if input_bytes is not None else None}
|
|
118
|
+
if model_ir["artifact"]["format"] == "onnx":
|
|
119
|
+
import onnx
|
|
120
|
+
import onnxruntime as ort
|
|
121
|
+
configuration["onnx_version"] = onnx.__version__
|
|
122
|
+
parsed = onnx.load_model_from_string(raw)
|
|
123
|
+
if any(t.data_location == onnx.TensorProto.EXTERNAL for t in parsed.graph.initializer):
|
|
124
|
+
raise ValueError("This collector requires inline ONNX data; external artifacts need an independently bound collector.")
|
|
125
|
+
try: parsed = onnx.shape_inference.infer_shapes(parsed)
|
|
126
|
+
except (ValueError, RuntimeError): pass
|
|
127
|
+
known = {v.name: v for v in list(parsed.graph.value_info) + list(parsed.graph.input) + list(parsed.graph.output)}
|
|
128
|
+
outputs = {v.name for v in parsed.graph.output}
|
|
129
|
+
accepted = []
|
|
130
|
+
for item in selected:
|
|
131
|
+
if item["region_ref"] != "region:scope:onnx:main_graph":
|
|
132
|
+
missing.append({"value_ref": item["id"], "reason": "nested_region_capture_not_supported"})
|
|
133
|
+
continue
|
|
134
|
+
if item["name"] not in outputs:
|
|
135
|
+
if item["name"] not in known:
|
|
136
|
+
missing.append({"value_ref": item["id"], "reason": "onnx_value_type_not_inferred"})
|
|
137
|
+
continue
|
|
138
|
+
parsed.graph.output.append(known[item["name"]])
|
|
139
|
+
outputs.add(item["name"])
|
|
140
|
+
accepted.append(item)
|
|
141
|
+
instrumented = parsed.SerializeToString()
|
|
142
|
+
instrumented_hash = sha(instrumented)
|
|
143
|
+
options = ort.SessionOptions()
|
|
144
|
+
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
|
|
145
|
+
options.intra_op_num_threads = options.inter_op_num_threads = 1
|
|
146
|
+
session = ort.InferenceSession(instrumented, options, providers=["CPUExecutionProvider"])
|
|
147
|
+
if not accepted:
|
|
148
|
+
raise ValueError("No requested activations are collectible within the bound; no run was made.")
|
|
149
|
+
arrays = session.run([v["name"] for v in accepted], feeds)
|
|
150
|
+
captures = [encode(item, array) for item, array in zip(accepted, arrays)]
|
|
151
|
+
runtime = {"name": "onnxruntime", "version": ort.__version__, "configured_providers": session.get_providers(), "device": "CPU"}
|
|
152
|
+
configuration["graph_optimizations"] = "disabled"
|
|
153
|
+
elif model_ir["artifact"]["format"] == "tflite":
|
|
154
|
+
from ai_edge_litert.interpreter import Interpreter, OpResolverType
|
|
155
|
+
from importlib.metadata import version
|
|
156
|
+
interpreter = Interpreter(model_content=raw, num_threads=1, experimental_preserve_all_tensors=True, experimental_op_resolver_type=OpResolverType.BUILTIN_WITHOUT_DEFAULT_DELEGATES)
|
|
157
|
+
for item in interpreter.get_input_details():
|
|
158
|
+
interpreter.resize_tensor_input(item["index"], feeds[item["name"]].shape, strict=True)
|
|
159
|
+
interpreter.allocate_tensors()
|
|
160
|
+
for item in interpreter.get_input_details(): interpreter.set_tensor(item["index"], feeds[item["name"]])
|
|
161
|
+
interpreter.invoke()
|
|
162
|
+
for item in selected:
|
|
163
|
+
if item["region_ref"] != "region:scope:tflite:subgraph:0":
|
|
164
|
+
missing.append({"value_ref": item["id"], "reason": "non_primary_subgraph_not_collected"})
|
|
165
|
+
continue
|
|
166
|
+
try: array = interpreter.get_tensor(item["native_index"])
|
|
167
|
+
except ValueError:
|
|
168
|
+
missing.append({"value_ref": item["id"], "reason": "runtime_did_not_preserve_tensor"})
|
|
169
|
+
continue
|
|
170
|
+
captures.append(encode(item, array))
|
|
171
|
+
runtime = {"name": "ai-edge-litert", "version": version("ai-edge-litert"), "configured_providers": ["builtin_without_default_delegates"], "device": "CPU"}
|
|
172
|
+
configuration["preserve_all_tensors"] = True
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError("Built-in capture supports ONNX and TFLite. Other runtimes can produce the same capture contract; runtime support is not inferred from file format.")
|
|
175
|
+
if sum(len(t["values"]) for t in captures) + sum(a.size for a in feeds.values()) > args.max_values:
|
|
176
|
+
raise ValueError("Actual run exceeds capture value budget; no partial success is emitted.")
|
|
177
|
+
if args.model.read_bytes() != raw:
|
|
178
|
+
raise ValueError("Artifact changed during capture.")
|
|
179
|
+
run = {"id": str(uuid.uuid4()), "started_at": started, "entry_region_ref": entry, "runtime": runtime,
|
|
180
|
+
"collector": {"name": "deepbom-local-activation-collector", "version": "1.0.0", "sha256": sha(Path(__file__).read_bytes())},
|
|
181
|
+
"execution": {"artifact_sha256": source["artifact_sha256"], "instrumented_artifact_sha256": instrumented_hash, "configuration_sha256": sha(json.dumps(configuration, sort_keys=True, separators=(",", ":")).encode()), "configuration": configuration},
|
|
182
|
+
"probe": {"kind": "custom" if args.inputs_npz else "synthetic_" + args.probe, "description": "User supplied numeric NPZ; preprocessing and representativeness are not independently established." if args.inputs_npz else "Synthetic " + args.probe + " input in serialized dtype; not a representative-data evaluation."}, "runtime_evidence": None}
|
|
183
|
+
capture = {"schema": "deepbom.activation_capture.v1", "source": source, "run": run, "inputs": [encode(item, feeds[item["name"]]) for item in inputs], "requested_value_refs": [v["id"] for v in requested], "captures": captures, "missing": missing}
|
|
184
|
+
encoded = json.dumps(capture, allow_nan=False, separators=(",", ":")) + "\n"
|
|
185
|
+
if len(encoded.encode()) > 16 * 1024 * 1024:
|
|
186
|
+
raise ValueError("Capture exceeds the 16 MiB import limit.")
|
|
187
|
+
with args.output.open("x") as output: output.write(encoded)
|
|
188
|
+
print(f"Captured {len(captures)}/{len(requested)} requested values; {len(missing)} unavailable. {args.output}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
try: main()
|
|
193
|
+
except Exception as error:
|
|
194
|
+
print(f"Activation capture failed: {error}", file=sys.stderr)
|
|
195
|
+
sys.exit(1)
|