explainer-video 0.0.0-stage → 0.4.1

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.
Files changed (153) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -2
  4. package/cordis.patch.yml +7 -0
  5. package/lib/index.js +35 -0
  6. package/package.json +56 -4
  7. package/plugin.json +13 -0
  8. package/skills/coursework-author/.synced-from +5 -0
  9. package/skills/coursework-author/SKILL.md +209 -0
  10. package/skills/craft-codeflow-flows/SKILL.md +116 -0
  11. package/skills/craft-codeflow-mine/SKILL.md +207 -0
  12. package/skills/craft-codeflow-plan/SKILL.md +65 -0
  13. package/skills/craft-codeflow-scene/SKILL.md +168 -0
  14. package/skills/craft-video-align/SKILL.md +109 -0
  15. package/skills/craft-video-assemble/SKILL.md +70 -0
  16. package/skills/craft-video-audio/SKILL.md +59 -0
  17. package/skills/craft-video-enrich/SKILL.md +46 -0
  18. package/skills/craft-video-plan/SKILL.md +57 -0
  19. package/skills/craft-video-publish/SKILL.md +79 -0
  20. package/skills/craft-video-render/SKILL.md +68 -0
  21. package/skills/craft-video-scene/SKILL.md +112 -0
  22. package/skills/craft-video-script/SKILL.md +80 -0
  23. package/skills/craft-video-thumbnail/SKILL.md +168 -0
  24. package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
  25. package/skills/explainer-video-from-codebase/SKILL.md +698 -0
  26. package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
  27. package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
  28. package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
  29. package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
  30. package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
  31. package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
  32. package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
  33. package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
  34. package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
  35. package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
  36. package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
  37. package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
  38. package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
  39. package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
  40. package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
  41. package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
  42. package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
  43. package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
  44. package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
  45. package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
  46. package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
  47. package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
  48. package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
  49. package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
  50. package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
  51. package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
  52. package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
  53. package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
  54. package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
  55. package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
  56. package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
  57. package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
  58. package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
  59. package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
  60. package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
  61. package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
  62. package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
  63. package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
  64. package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
  65. package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
  66. package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
  67. package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
  68. package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
  69. package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
  70. package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
  71. package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
  72. package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
  73. package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
  74. package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
  75. package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
  76. package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
  77. package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
  78. package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
  79. package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
  80. package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
  81. package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
  82. package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
  83. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
  84. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
  85. package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
  86. package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
  87. package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
  88. package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
  89. package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
  90. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
  91. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
  92. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
  93. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
  94. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
  95. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
  96. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
  97. package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
  98. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
  99. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
  100. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
  101. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  102. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  103. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  104. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  105. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
  106. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
  107. package/skills/explainer-video-from-coursework/ARCHITECTURE.md +61 -0
  108. package/skills/explainer-video-from-coursework/PIPELINE.md +369 -0
  109. package/skills/explainer-video-from-coursework/ROADMAP.md +727 -0
  110. package/skills/explainer-video-from-coursework/SERIES.md +243 -0
  111. package/skills/explainer-video-from-coursework/SETUP.md +105 -0
  112. package/skills/explainer-video-from-coursework/SKILL.md +998 -0
  113. package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
  114. package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
  115. package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
  116. package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
  117. package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
  118. package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
  119. package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
  120. package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
  121. package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
  122. package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
  123. package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
  124. package/skills/explainer-video-from-coursework/src/thumbnail.html +119 -0
  125. package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
  126. package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
  127. package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
  128. package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
  129. package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
  130. package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
  131. package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
  132. package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
  133. package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
  134. package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
  135. package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
  136. package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
  137. package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
  138. package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
  139. package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
  140. package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
  141. package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
  142. package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
  143. package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
  144. package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
  145. package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
  146. package/skills/explainer-video-from-coursework/tools/thumbnail.mjs +224 -0
  147. package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
  148. package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
  149. package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
  150. package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
  151. package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
  152. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
  153. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
@@ -0,0 +1,330 @@
1
+ """trace.mjs's Python collector: record which in-repo functions called which while the repo's own test command runs.
2
+
3
+ Loaded through PYTHONPATH from a directory outside the repo, inside trace.mjs's sandbox. On every Python call it records
4
+ (nearest in-repo caller, in-repo callee, current test): library frames between the two are skipped, so a call through a
5
+ decorator, a framework or the event loop is attributed to the in-repo code that started it. The current test is set by
6
+ fmtrace_plugin (pytest) or by the TestCase.run wrapper below (unittest), so a call a framework makes (a fixture, a DI
7
+ provider, a test client's handler thread) is still attributed to the test it ran under. It also records each test's
8
+ network attempts (audit hooks), so a test that failed on a connect the sandbox denied can be listed as denied, as can
9
+ one that failed on reading a path the sandbox hides (the user's home folder, the original repo). Imports an editable
10
+ install points into the original repo are sent to the copy.
11
+ Writes one JSON file per process at exit (or on SIGTERM, when trace.mjs's time limit stops the run). Never imports repo
12
+ code itself.
13
+
14
+ Environment: FM_TRACE_ROOT (the scratch copy of the repo), FM_TRACE_OUT (the output directory), FM_TRACE_RUNNER
15
+ ("pytest" or "unittest"), FM_TRACE_ORIGIN (where the original repo was) and FM_TRACE_HIDDEN (the paths the sandbox
16
+ hides), both os.pathsep-separated."""
17
+ import atexit, errno, json, os, signal, sys, threading
18
+
19
+
20
+ def _dirs(var):
21
+ return tuple(os.path.realpath(r) + os.sep for r in os.environ.get(var, "").split(os.pathsep) if r)
22
+
23
+
24
+ ROOTS = _dirs("FM_TRACE_ROOT")
25
+ ORIGINS = tuple(sorted({o for r in os.environ.get("FM_TRACE_ORIGIN", "").split(os.pathsep) if r
26
+ for o in (os.path.abspath(r) + os.sep, os.path.realpath(r) + os.sep)}, key=len, reverse=True))
27
+ HIDDEN = _dirs("FM_TRACE_HIDDEN")
28
+ OUT = os.environ["FM_TRACE_OUT"]
29
+ RUNNER = os.environ.get("FM_TRACE_RUNNER")
30
+ # an interpreter or virtualenv that lives under a repo root is library code, not repo code
31
+ _LIB = tuple({os.path.realpath(p) + os.sep for p in (sys.prefix, sys.base_prefix, sys.exec_prefix)})
32
+ _FOLD = (".<lambda>", ".<genexpr>", ".<listcomp>", ".<dictcomp>", ".<setcomp>")
33
+ _keys = {}
34
+ _pairs = {} # (caller, callee, test) -> calls
35
+ _net = {} # test -> [network attempts]
36
+ OUTCOMES = {} # test -> {phase: outcome, ...}, filled by fmtrace_plugin or the unittest wrapper
37
+ COLLECTED = [None]
38
+ COLLECT_ERRORS = []
39
+ FIXTURE_ERRORS = [] # unittest's failing class and module fixtures: not tests, so not in OUTCOMES
40
+ FIXTURE = [None] # (name, phase) of the unittest class or module fixture running outside any test
41
+ CURRENT = [None] # the running test's key, set around its setup, call and teardown
42
+ _done = [False]
43
+ _forked = [False]
44
+
45
+
46
+ def _rel(f):
47
+ if not f.endswith(".py") or f.startswith(_LIB) or os.sep + "site-packages" + os.sep in f or os.sep + "dist-packages" + os.sep in f:
48
+ return None
49
+ for r in ROOTS:
50
+ if f.startswith(r):
51
+ return f[len(r):]
52
+ return None
53
+
54
+
55
+ def _key(code):
56
+ # keyed by identity: code objects compare by value (not by file), so twin bodies in two files would collide;
57
+ # the stored reference keeps the id from being reused
58
+ hit = _keys.get(id(code))
59
+ if hit is not None:
60
+ return hit[1]
61
+ rel = _rel(code.co_filename)
62
+ k = False
63
+ if rel is not None:
64
+ q = getattr(code, "co_qualname", code.co_name)
65
+ # comprehensions, lambdas and generator bodies belong to the function that holds them
66
+ while q.endswith(_FOLD) or q in ("<lambda>", "<genexpr>"):
67
+ q = q.rsplit(".<locals>.", 1)[0] if ".<locals>." in q else "<module>"
68
+ k = (rel, q)
69
+ _keys[id(code)] = (code, k)
70
+ return k
71
+
72
+
73
+ def _prof(frame, event, arg):
74
+ if event != "call":
75
+ return
76
+ callee = _key(frame.f_code)
77
+ if not callee:
78
+ return
79
+ caller = None
80
+ b = frame.f_back
81
+ while b is not None:
82
+ caller = _key(b.f_code)
83
+ if caller:
84
+ break
85
+ b = b.f_back
86
+ p = (caller or None, callee, CURRENT[0])
87
+ _pairs[p] = _pairs.get(p, 0) + 1
88
+
89
+
90
+ def _audit(event, args):
91
+ if event in ("socket.connect", "socket.getaddrinfo", "socket.gethostbyname", "socket.gethostbyaddr", "socket.sendto"):
92
+ seen = _net.setdefault(FIXTURE[0][0] if FIXTURE[0] else CURRENT[0], [])
93
+ if len(seen) < 5:
94
+ target = args[1] if event in ("socket.connect", "socket.sendto") else args[0] if args else None
95
+ if event == "socket.getaddrinfo" and len(args) > 1:
96
+ target = (args[0], args[1])
97
+ what = event.split(".", 1)[1] + " " + str(target)[:120]
98
+ if what not in seen:
99
+ seen.append(what)
100
+
101
+
102
+ def _dump():
103
+ if _done[0]:
104
+ return
105
+ _done[0] = True
106
+ sys.setprofile(None)
107
+ threading.setprofile(None)
108
+ rows = [{"from": list(c) if c else None, "to": list(t), "test": k, "n": n} for (c, t, k), n in _pairs.items()]
109
+ data = {"argv": sys.argv, "python": sys.version.split()[0], "pairs": rows,
110
+ "network": {k: v for k, v in _net.items() if k is not None}, "outcomes": OUTCOMES,
111
+ "collected": COLLECTED[0], "collectErrors": COLLECT_ERRORS, "fixtureErrors": FIXTURE_ERRORS}
112
+ name = "trace-%d-%s.json" % (os.getpid(), os.urandom(4).hex())
113
+ with open(os.path.join(OUT, name), "w") as fh:
114
+ json.dump(data, fh)
115
+
116
+
117
+ def _to_copy(p):
118
+ # the interpreter's own paths stay: its virtualenv may live in the original repo, and the sandbox exposes it there
119
+ if isinstance(p, str) and not (os.path.realpath(p) + os.sep).startswith(_LIB):
120
+ for o in ORIGINS:
121
+ if p == o[:-1] or p.startswith(o):
122
+ return ROOTS[0] + p[len(o):]
123
+ return p
124
+
125
+
126
+ def _pth_paths():
127
+ # the path lines of the site dirs' .pth files: site skipped every one that points into the original repo, since
128
+ # the sandbox hides it
129
+ import site
130
+ for d in site.getsitepackages():
131
+ try:
132
+ names = sorted(n for n in os.listdir(d) if n.endswith(".pth"))
133
+ except OSError:
134
+ continue
135
+ for n in names:
136
+ try:
137
+ with open(os.path.join(d, n), encoding="utf-8") as fh:
138
+ lines = fh.read().splitlines()
139
+ except (OSError, UnicodeDecodeError):
140
+ continue
141
+ for line in lines:
142
+ line = line.strip()
143
+ if line and not line.startswith(("#", "import ", "import\t")):
144
+ yield os.path.join(d, line)
145
+
146
+
147
+ def _redirect_origin():
148
+ # an editable install points into the original repo, which the sandbox hides: a path entry (a .pth line), a
149
+ # setuptools finder's MAPPING and NAMESPACES, or an `editables` redirect; each is sent to the copy instead
150
+ sys.path[:] = [_to_copy(p) for p in sys.path]
151
+ for p in _pth_paths():
152
+ moved = _to_copy(p)
153
+ if moved != p and moved not in sys.path and os.path.isdir(moved):
154
+ sys.path.append(moved)
155
+ for name, mod in list(sys.modules.items()):
156
+ if name.startswith("__editable__"):
157
+ for k, v in (getattr(mod, "MAPPING", None) or {}).items():
158
+ mod.MAPPING[k] = _to_copy(v)
159
+ for k, v in (getattr(mod, "NAMESPACES", None) or {}).items():
160
+ mod.NAMESPACES[k] = [_to_copy(x) for x in v]
161
+ for finder in sys.meta_path:
162
+ redirects = getattr(finder, "_redirections", None)
163
+ if isinstance(redirects, dict):
164
+ for k, v in redirects.items():
165
+ redirects[k] = _to_copy(v)
166
+
167
+
168
+ def _hidden(path):
169
+ return isinstance(path, str) and os.path.realpath(path).startswith(HIDDEN)
170
+
171
+
172
+ def denied(exc):
173
+ """Did this exception (or one it came from) come from the sandbox: a permission error, or a hidden path not found
174
+ (bwrap hides a path by mounting an empty dir over it, so a read of it fails as missing)."""
175
+ seen = set()
176
+ while exc is not None and id(exc) not in seen:
177
+ seen.add(id(exc))
178
+ if isinstance(exc, OSError) and (exc.errno in (errno.EPERM, errno.EACCES) or
179
+ (exc.errno == errno.ENOENT and (_hidden(exc.filename) or _hidden(exc.filename2)))):
180
+ return True
181
+ exc = exc.__cause__ or exc.__context__
182
+ return False
183
+
184
+
185
+ def _denied_line(line):
186
+ # unittest keeps only the formatted traceback: its last line
187
+ if line.startswith("PermissionError") or "[Errno 1] Operation not permitted" in line or "[Errno 13] Permission denied" in line:
188
+ return True
189
+ return line.startswith("FileNotFoundError") and any(h in line or h[:-1] + "'" in line for h in HIDDEN)
190
+
191
+
192
+ def _reset_in_child():
193
+ # a forked child starts with a copy of the parent's records; it must write only its own
194
+ _pairs.clear()
195
+ _net.clear()
196
+ OUTCOMES.clear()
197
+ COLLECT_ERRORS.clear()
198
+ FIXTURE_ERRORS.clear()
199
+ COLLECTED[0] = None
200
+ _forked[0] = True
201
+
202
+
203
+ def _on_term(signum, frame):
204
+ # the test the time limit cut off, in the process that ran it (a forked worker's SIGTERM is not a cut-off): the
205
+ # phase it was in fails, as pytest counts it; under unittest the test is an error
206
+ if FIXTURE[0] and not _forked[0]:
207
+ FIXTURE_ERRORS.append({"nodeid": FIXTURE[0][0], "phase": FIXTURE[0][1], "message": "cut off by the time limit"})
208
+ elif CURRENT[0] is not None and not _forked[0]:
209
+ rec = OUTCOMES.setdefault(CURRENT[0], {})
210
+ rec.setdefault("message", "cut off by the time limit")
211
+ if RUNNER == "pytest":
212
+ phases = ("setup", "call", "teardown") if rec.get("setup", "passed") == "passed" else ("setup", "teardown")
213
+ phase = next((p for p in phases if p not in rec), None)
214
+ if phase:
215
+ rec[phase] = "error" if phase == "call" else "failed"
216
+ else:
217
+ rec.setdefault("call", "error")
218
+ _dump()
219
+ os._exit(128 + signum)
220
+
221
+
222
+ def _wrap_unittest():
223
+ import traceback
224
+ import unittest.case
225
+ import unittest.result
226
+ import unittest.suite
227
+
228
+ run = unittest.case.TestCase.run
229
+ kinds = ("errors", "failures", "skipped", "expectedFailures", "unexpectedSuccesses")
230
+
231
+ def counts(result):
232
+ return {k: len(getattr(result, k, ()) or ()) for k in kinds}
233
+
234
+ # only the outermost test records: a suite a test runs inside itself is that test's own business
235
+ inside = [False]
236
+
237
+ def traced_run(self, result=None):
238
+ if inside[0]:
239
+ return run(self, result)
240
+ key = "unittest::" + self.id()
241
+ before = counts(result) if result is not None else None
242
+ CURRENT[0] = key
243
+ inside[0] = True
244
+ if before is not None:
245
+ OUTCOMES[key] = {}
246
+ try:
247
+ return run(self, result)
248
+ finally:
249
+ CURRENT[0] = None
250
+ inside[0] = False
251
+ if before is not None:
252
+ after = counts(result)
253
+ grew = [k for k in kinds if after[k] > before[k]]
254
+ outcome = {"errors": "error", "failures": "failed", "skipped": "skipped",
255
+ "expectedFailures": "xfailed", "unexpectedSuccesses": "xpassed"}.get(grew[0] if grew else "", "passed")
256
+ rec = {"call": outcome}
257
+ if outcome in ("error", "failed"):
258
+ tb = getattr(result, grew[0])[-1][1]
259
+ last = tb.strip().splitlines()[-1] if tb.strip() else ""
260
+ rec["message"] = last[:200]
261
+ rec["permission"] = _denied_line(last)
262
+ OUTCOMES[key] = rec
263
+
264
+ unittest.case.TestCase.run = traced_run
265
+
266
+ # a failing setUpModule, setUpClass, tearDownClass or tearDownModule is reported outside any TestCase.run, under
267
+ # unittest's own name for it (`setUpClass (pkg.mod.Class)`): record it as a fixture error (in its setup or teardown
268
+ # phase), and note the fixture while it runs, so its network attempts and a cut-off there are recorded under that
269
+ # name too
270
+ add_error = unittest.result.TestResult.addError
271
+
272
+ def traced_add_error(self, test, err):
273
+ if isinstance(test, unittest.suite._ErrorHolder) and not inside[0]:
274
+ last = traceback.format_exception_only(err[0], err[1])[-1].strip()
275
+ FIXTURE_ERRORS.append({"nodeid": "unittest::" + test.id(), "phase": "setup" if test.id().startswith("setUp") else "teardown",
276
+ "message": last[:200], "permission": denied(err[1])})
277
+ return add_error(self, test, err)
278
+
279
+ unittest.result.TestResult.addError = traced_add_error
280
+
281
+ def strclass(cls):
282
+ return "%s.%s" % (cls.__module__, cls.__qualname__)
283
+
284
+ def previous(result):
285
+ return getattr(result, "_previousTestClass", None)
286
+
287
+ def fixture(method, name, parent):
288
+ handle = getattr(unittest.suite.TestSuite, method)
289
+
290
+ def traced(self, *args):
291
+ p = None if inside[0] else parent(*args)
292
+ if not p:
293
+ return handle(self, *args)
294
+ prev, FIXTURE[0] = FIXTURE[0], ("unittest::%s (%s)" % (name, p), "setup" if name.startswith("setUp") else "teardown")
295
+ try:
296
+ return handle(self, *args)
297
+ finally:
298
+ FIXTURE[0] = prev
299
+
300
+ setattr(unittest.suite.TestSuite, method, traced)
301
+
302
+ fixture("_handleClassSetUp", "setUpClass", lambda test, result: strclass(test.__class__))
303
+ fixture("_tearDownPreviousClass", "tearDownClass", lambda test, result: previous(result) and strclass(previous(result)))
304
+ fixture("_handleModuleFixture", "setUpModule", lambda test, result: test.__class__.__module__)
305
+ fixture("_handleModuleTearDown", "tearDownModule", lambda result: previous(result) and previous(result).__module__)
306
+
307
+
308
+ def _run_shadowed_sitecustomize():
309
+ # this module shadows the interpreter's own sitecustomize (Homebrew's adds its site-packages there): run that one too
310
+ here = os.path.dirname(os.path.realpath(__file__))
311
+ for d in sys.path:
312
+ p = os.path.join(d or ".", "sitecustomize.py")
313
+ if os.path.realpath(d or ".") != here and os.path.isfile(p):
314
+ import runpy
315
+
316
+ runpy.run_path(p, run_name="_shadowed_sitecustomize")
317
+ return
318
+
319
+
320
+ _run_shadowed_sitecustomize()
321
+ _redirect_origin()
322
+ if RUNNER == "unittest":
323
+ _wrap_unittest()
324
+ os.register_at_fork(after_in_child=_reset_in_child)
325
+ if signal.getsignal(signal.SIGTERM) == signal.SIG_DFL:
326
+ signal.signal(signal.SIGTERM, _on_term)
327
+ sys.addaudithook(_audit)
328
+ atexit.register(_dump)
329
+ threading.setprofile(_prof)
330
+ sys.setprofile(_prof)
@@ -0,0 +1,73 @@
1
+ #!/bin/sh
2
+ # linux-bwrap.sh — trace.mjs's Linux sandbox (bubblewrap), for running a repo's test command.
3
+ #
4
+ # Usage: linux-bwrap.sh <scratch> <hidden paths, one per line> <read-only paths, one per line> <command> [args...]
5
+ #
6
+ # - no network at all: a new network namespace (no route out, and its own empty loopback, so nothing on
7
+ # the host's localhost is reachable); a seccomp filter makes socket(AF_UNIX, ...) fail with EPERM, so no
8
+ # unix socket (docker.sock, a database or agent socket) can be connected to, wherever it lives — a
9
+ # read-only mount does not stop a connect. socketpair() still works, so asyncio does;
10
+ # - no writes outside scratch: / is mounted read-only, the dirs that hold host state (/run, /tmp, /var/tmp,
11
+ # home dirs) and the hidden paths (the user's home folder and the original repo) are replaced by empty
12
+ # tmpfs mounts, so nothing in them can be read, and <scratch> is the only writable host path;
13
+ # - the read-only paths (the interpreter and its virtualenv, or node and the repo's node_modules) are mounted back over the hidden dirs;
14
+ # lib/sandbox.mjs never passes one that contains <scratch> or a hidden path;
15
+ # - new user, pid, ipc, uts and cgroup namespaces. No --die-with-parent: trace.mjs's time limit signals the whole
16
+ # process group, and the collector must outlive bwrap long enough to write the partial trace.
17
+ # The caller passes an empty environment; bwrap hands it to the command unchanged.
18
+ set -eu
19
+ scratch=$1
20
+ hidden=$2
21
+ ro=$3
22
+ shift 3
23
+
24
+ # the seccomp filter, as the classic-BPF program bwrap --seccomp loads (struct sock_filter, little-endian):
25
+ # arch is not this one -> kill · x32 syscall -> kill · socket(AF_UNIX, ...) -> EPERM · anything else -> allow
26
+ case $(uname -m) in
27
+ x86_64) arch=$((0xC000003E)); socket_nr=41 ;;
28
+ aarch64) arch=$((0xC00000B7)); socket_nr=198 ;;
29
+ *) echo "linux-bwrap.sh: no seccomp filter for $(uname -m)" >&2; exit 70 ;;
30
+ esac
31
+ byte() { printf "\\$(printf '%03o' "$1")"; }
32
+ ins() { byte $(($1 & 255)); byte $((($1 >> 8) & 255)); byte "$2"; byte "$3"
33
+ byte $(($4 & 255)); byte $((($4 >> 8) & 255)); byte $((($4 >> 16) & 255)); byte $((($4 >> 24) & 255)); }
34
+ LD=32 JEQ=21 JGE=53 RET=6 KILL=$((0x80000000)) ALLOW=$((0x7FFF0000)) EPERM=$((0x00050001))
35
+ bpf="$scratch/.fmtrace-seccomp.bpf"
36
+ {
37
+ ins $LD 0 0 4 # 0 A = arch
38
+ ins $JEQ 1 0 "$arch" # 1 this arch -> 3
39
+ ins $RET 0 0 "$KILL" # 2
40
+ ins $LD 0 0 0 # 3 A = syscall number
41
+ ins $JGE 4 0 $((0x40000000)) # 4 x32 -> 9
42
+ ins $JEQ 0 2 "$socket_nr" # 5 not socket() -> 8
43
+ ins $LD 0 0 16 # 6 A = low 32 bits of the domain argument
44
+ ins $JEQ 2 0 1 # 7 AF_UNIX -> 10
45
+ ins $RET 0 0 "$ALLOW" # 8
46
+ ins $RET 0 0 "$KILL" # 9
47
+ ins $RET 0 0 "$EPERM" # 10
48
+ } > "$bpf"
49
+
50
+ # the mount options are built back to front (each `set --` prepends), so bwrap applies them in this order:
51
+ # / read-only, /dev, /proc, the host-state dirs, the hidden paths, scratch, the read-only paths, then runs the command
52
+ set -- -- "$@"
53
+ set -f
54
+ IFS='
55
+ '
56
+ for p in $ro; do
57
+ set -- --ro-bind "$p" "$p" "$@"
58
+ done
59
+ unset IFS
60
+ set +f
61
+ set -- --bind "$scratch" "$scratch" "$@"
62
+ set -f
63
+ IFS='
64
+ '
65
+ for p in $hidden; do
66
+ set -- --tmpfs "$p" "$@"
67
+ done
68
+ unset IFS
69
+ set +f
70
+ for d in /srv /media /mnt /root /home /var/tmp /tmp /run; do
71
+ if [ -d "$d" ] && [ ! -L "$d" ]; then set -- --tmpfs "$d" "$@"; fi
72
+ done
73
+ exec bwrap --unshare-all --seccomp 3 --ro-bind / / --dev /dev --proc /proc "$@" 3<"$bpf"
@@ -0,0 +1,41 @@
1
+ ; macos-nonet.sb — trace.mjs's macOS sandbox (sandbox-exec), for running a repo's test command.
2
+ ; SCRATCH (passed with -D SCRATCH=<realpath>) is the only place a write may land. lib/sandbox.mjs appends the read
3
+ ; rules for each run: no reads under the user's home folder or the original repo, except scratch, the interpreter
4
+ ; and its virtualenv. A later rule overrides an earlier one.
5
+ (version 1)
6
+ (allow default)
7
+
8
+ ; no network at all: no IP traffic (localhost included), no connects to unix sockets (the DNS resolver,
9
+ ; docker.sock, database sockets); socketpair() still works, so asyncio does
10
+ (deny network-outbound (remote ip))
11
+ (deny network-inbound (local ip))
12
+ (deny network-bind (local ip))
13
+ (deny network-outbound (remote unix-socket))
14
+
15
+ ; no handing work to a process outside the sandbox, which would run with none of these rules: no opening a URL, file
16
+ ; or app through LaunchServices (open, NSWorkspace), no Apple Events (osascript, `tell application`), no launchd jobs
17
+ ; (launchctl submit, SMJobSubmit), neither through the services behind them nor through the tools that use them
18
+ (deny lsopen)
19
+ (deny appleevent-send)
20
+ (deny job-creation)
21
+ (deny mach-lookup
22
+ (global-name "com.apple.coreservices.launchservicesd")
23
+ (global-name "com.apple.lsd.open")
24
+ (global-name "com.apple.lsd.openurl")
25
+ (global-name "com.apple.lsd.modifydb")
26
+ (global-name "com.apple.coreservices.appleevents")
27
+ (global-name "com.apple.xpc.smd"))
28
+ (deny process-exec
29
+ (literal "/usr/bin/open")
30
+ (literal "/usr/bin/osascript")
31
+ (literal "/bin/launchctl"))
32
+
33
+ ; writes only inside the scratch dir (and the null and terminal devices)
34
+ (deny file-write*)
35
+ (allow file-write*
36
+ (subpath (param "SCRATCH"))
37
+ (literal "/dev/null")
38
+ (literal "/dev/zero")
39
+ (literal "/dev/tty")
40
+ (literal "/dev/dtracehelper")
41
+ (regex #"^/dev/fd/"))