syft-restrict 0.1.0__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.
Files changed (37) hide show
  1. syft_restrict-0.1.0/.gitignore +200 -0
  2. syft_restrict-0.1.0/PKG-INFO +109 -0
  3. syft_restrict-0.1.0/README.md +99 -0
  4. syft_restrict-0.1.0/docs/blacklist.md +257 -0
  5. syft_restrict-0.1.0/docs/code-layout.md +23 -0
  6. syft_restrict-0.1.0/docs/verify.md +363 -0
  7. syft_restrict-0.1.0/examples/gemma_inference.certificate.json +86 -0
  8. syft_restrict-0.1.0/examples/gemma_inference.obfuscated.py +414 -0
  9. syft_restrict-0.1.0/examples/gemma_inference.py +414 -0
  10. syft_restrict-0.1.0/examples/gemma_inference_marked.certificate.json +86 -0
  11. syft_restrict-0.1.0/examples/gemma_inference_marked.obfuscated.py +474 -0
  12. syft_restrict-0.1.0/examples/gemma_inference_marked.py +510 -0
  13. syft_restrict-0.1.0/examples/generate.py +103 -0
  14. syft_restrict-0.1.0/examples/generate_marked.py +58 -0
  15. syft_restrict-0.1.0/pyproject.toml +18 -0
  16. syft_restrict-0.1.0/src/syft_restrict/__init__.py +32 -0
  17. syft_restrict-0.1.0/src/syft_restrict/astutil.py +153 -0
  18. syft_restrict-0.1.0/src/syft_restrict/errors.py +28 -0
  19. syft_restrict-0.1.0/src/syft_restrict/markers.py +174 -0
  20. syft_restrict-0.1.0/src/syft_restrict/obfuscator.py +260 -0
  21. syft_restrict-0.1.0/src/syft_restrict/policy.py +182 -0
  22. syft_restrict-0.1.0/src/syft_restrict/runner.py +106 -0
  23. syft_restrict-0.1.0/src/syft_restrict/verifier.py +1023 -0
  24. syft_restrict-0.1.0/tests/fixtures/compliant_model.py +42 -0
  25. syft_restrict-0.1.0/tests/fixtures/marked_model.py +42 -0
  26. syft_restrict-0.1.0/tests/obfuscate/__init__.py +0 -0
  27. syft_restrict-0.1.0/tests/obfuscate/test_obfuscate.py +110 -0
  28. syft_restrict-0.1.0/tests/test_markers.py +239 -0
  29. syft_restrict-0.1.0/tests/test_run.py +124 -0
  30. syft_restrict-0.1.0/tests/verify/__init__.py +0 -0
  31. syft_restrict-0.1.0/tests/verify/conftest.py +35 -0
  32. syft_restrict-0.1.0/tests/verify/helpers.py +29 -0
  33. syft_restrict-0.1.0/tests/verify/test_bypasses.py +617 -0
  34. syft_restrict-0.1.0/tests/verify/test_disallowed.py +328 -0
  35. syft_restrict-0.1.0/tests/verify/test_ranges.py +20 -0
  36. syft_restrict-0.1.0/tests/verify/test_whitelist.py +321 -0
  37. syft_restrict-0.1.0/tests/verify/test_whitelisted_lib.py +146 -0
@@ -0,0 +1,200 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ credentials/
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ .python-version
87
+
88
+ # python stuff
89
+ *__pycache__*
90
+ *.pyc
91
+ *.egg-info/
92
+ *.swp
93
+ *.swo
94
+ *.ipynb_checkpoints*
95
+ **.pytest_cache/
96
+ **/pip-wheel-metadata
97
+
98
+ # pipenv
99
+ Pipfile.lock
100
+
101
+ # poetry
102
+ poetry.lock
103
+
104
+ # pdm
105
+ .pdm.toml
106
+
107
+ # PEP 582
108
+ __pypackages__/
109
+
110
+ # Celery stuff
111
+ celerybeat-schedule
112
+ celerybeat.pid
113
+
114
+ # SageMath parsed files
115
+ *.sage.py
116
+
117
+ # Environments
118
+ .env
119
+ .venv
120
+ env/
121
+ venv/
122
+ ENV/
123
+ env.bak/
124
+ venv.bak/
125
+
126
+ # Spyder project settings
127
+ .spyderproject
128
+ .spyproject
129
+
130
+ # Rope project settings
131
+ .ropeproject
132
+
133
+ # mkdocs documentation
134
+ /site
135
+
136
+ # mypy
137
+ .mypy_cache/
138
+ .dmypy.json
139
+ dmypy.json
140
+
141
+ # Pyre type checker
142
+ .pyre/
143
+
144
+ # pytype static type analyzer
145
+ .pytype/
146
+
147
+ # Cython debug symbols
148
+ cython_debug/
149
+
150
+ # PyCharm
151
+ .idea/
152
+
153
+ # VS Code
154
+ .vscode/
155
+
156
+ # macOS
157
+ .DS_Store
158
+
159
+ # Google Drive credentials
160
+ credentials.json
161
+ client_secret*.json
162
+ token.pickle
163
+ token.json
164
+ *creds.json
165
+
166
+ # SyftBox related
167
+ ~/SyftBox/
168
+ SyftBox/
169
+
170
+ # Temporary files
171
+ *.tmp
172
+ *.bak
173
+ *.swp
174
+ *~
175
+
176
+ # Claude AI settings
177
+ .claude/*
178
+ !.claude/settings.json
179
+ CLAUDE.md
180
+
181
+ # CI test outputs
182
+ test_outputs/
183
+ test_suite_output.log
184
+
185
+
186
+ # Notebooks
187
+ notebooks/e2e/sales_mock.csv
188
+ notebooks/e2e/sales_private.csv
189
+ notebooks/e2e/readme.mdpackages/syft-bg/docs/sync-service.md
190
+ packages/syft-bg/docs/sync-review-notes.md
191
+ notebooks/ai_audit/internal/data/pdfs
192
+ notebooks/ai_audit/internal/data/manifest.csv
193
+ notebooks/ai_audit/internal/data/readme.md
194
+
195
+ .agents/
196
+ AGENTS.md
197
+
198
+ # Local JupyterLab state (jupyter MCP server runs from repo root)
199
+ .jupyter/
200
+ .jupyter_ystore.db
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: syft-restrict
3
+ Version: 0.1.0
4
+ Summary: Verify + obfuscate: prove JAX/Flax inference code only does math (no data theft) while hiding the model architecture
5
+ Author-email: OpenMined <info@openmined.org>
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: pydantic>=2
9
+ Description-Content-Type: text/markdown
10
+
11
+ # syft-restrict
12
+
13
+ A **static analyzer** for Python source files that **default-denies** dynamic
14
+ constructs in a **private** region, typically the model logic that must be kept private, while allowing the **public** region to be reviewed by the data owner in an **obfuscated** copy.
15
+
16
+ ## Overview
17
+
18
+ `syft-restrict` splits a Python file into two regions:
19
+
20
+ - **Private** — the lines the user marks as private, typically the model logic that
21
+ must not be readable. Mark them with `# syft-restrict: obfuscate-start` /
22
+ `# syft-restrict: obfuscate-end` or `# syft-restrict: hide-start` / `# syft-restrict: hide-end` comments in the source.
23
+ - **Public** — every other line outside the marked ranges: imports, data loading, wrappers. It is
24
+ never checked or obfuscated. The recipient of the obfuscated file can read it directly.
25
+
26
+ Given that split, `syft-restrict` executes the following two steps:
27
+
28
+ - **The verifier** statically walks the private region and analyzes it against a default-deny
29
+ policy. Every construct must be explicitly allowed, or verification fails
30
+ and reports each offending line.
31
+ - **The obfuscator** runs after a clean verify. It creates an obfuscated copy by rewriting the private
32
+ region — renaming identifiers, blanking constants, replacing lines with `■■■■■■■■` — so the logic can't be read
33
+ back out, while the public region is copied through untouched.
34
+
35
+ The **verifier** is inspired by
36
+ [**RestrictedPython**](https://github.com/zopefoundation/RestrictedPython), but
37
+ differs in a few ways:
38
+
39
+ - **It analyzes — it doesn't run.** It never executes the source. Only the static checks decide pass/fail.
40
+ - The public code may **import allowlisted libraries** and call into them.
41
+ Imports are not allowed in private code, and any library call must be
42
+ **explicitly allow-listed**.
43
+ - **Dynamic Python lives in the public part.** Anything dynamic the author needs
44
+ (file/tokenizer loading, the generation loop, wrappers around library methods)
45
+ must be written in the **public** region — where it can be reviewed directly — and
46
+ may be **called by** the private part.
47
+
48
+ ## Usage
49
+
50
+ Mark the private region in the source with comments:
51
+
52
+ ```python
53
+ # syft-restrict: obfuscate-start
54
+ def attention(x):
55
+ ...
56
+ # syft-restrict: obfuscate-end
57
+
58
+ MODEL_ID = "gemma-2b" # syft-restrict: hide
59
+ ```
60
+
61
+ _See [docs/verify.md](docs/verify.md#public-vs-private) for the full marker syntax (single-line markers, nesting rules, and error cases)._
62
+
63
+ Then run:
64
+
65
+ ```python
66
+ import syft_restrict as restrict
67
+
68
+ result = restrict.run(
69
+ "gemma_inference.py",
70
+ allow_functions=["jax.*", "flax.linen.*"], # functions callable BY NAME (path-resolved)
71
+ allow_operators=["arithmetic", "indexing", "comparison"], # operators allowed ON A VALUE
72
+ )
73
+ # On success: writes gemma_inference.obfuscated.py and returns result.certificate.
74
+ # On a policy violation: raises PolicyViolation naming each offending line.
75
+ # If the file has no markers and obfuscate=/hide= are both omitted: raises MarkerError.
76
+ ```
77
+
78
+ `obfuscate=`/`hide=` still work as an explicit escape hatch — pass either one (even an
79
+ empty list) and marker scanning is skipped entirely in favor of your own 1-based line
80
+ ranges:
81
+
82
+ ```python
83
+ result = restrict.run(
84
+ "gemma_inference.py",
85
+ obfuscate=[[22, 93], [99, 280]], # 1-based ranges: identifiers renamed, constants blanked
86
+ hide=[], # 1-based ranges: whole line replaced with ■■■■■■■■
87
+ allow_functions=["jax.*", "flax.linen.*"],
88
+ allow_operators=["arithmetic", "indexing", "comparison"],
89
+ )
90
+ ```
91
+
92
+ This is how **[examples/gemma_inference.py](examples/gemma_inference.py)** generates
93
+ **[examples/gemma_inference.obfuscated.py](examples/gemma_inference.obfuscated.py)** —
94
+ see [examples/generate.py](examples/generate.py).
95
+
96
+ The same model marked up with comment blocks instead of numeric ranges lives in
97
+ **[examples/gemma_inference_marked.py](examples/gemma_inference_marked.py)** /
98
+ [examples/generate_marked.py](examples/generate_marked.py).
99
+
100
+ If syft-restrict was successfully executed on the true original file, the obfuscated file can be safely shared without exposing the private section.
101
+
102
+ Pass `strict=False` to `run` to get a `RunResult` with `.ok` / `.violations`
103
+ instead of an exception.
104
+
105
+ ## Documentation
106
+
107
+ - [docs/verify.md](docs/verify.md) — how verification works and what private code may do (allow side).
108
+ - [docs/blacklist.md](docs/blacklist.md) — default-deny catalog and violation codes (deny side).
109
+ - [docs/code-layout.md](docs/code-layout.md) — source modules and test layout.
@@ -0,0 +1,99 @@
1
+ # syft-restrict
2
+
3
+ A **static analyzer** for Python source files that **default-denies** dynamic
4
+ constructs in a **private** region, typically the model logic that must be kept private, while allowing the **public** region to be reviewed by the data owner in an **obfuscated** copy.
5
+
6
+ ## Overview
7
+
8
+ `syft-restrict` splits a Python file into two regions:
9
+
10
+ - **Private** — the lines the user marks as private, typically the model logic that
11
+ must not be readable. Mark them with `# syft-restrict: obfuscate-start` /
12
+ `# syft-restrict: obfuscate-end` or `# syft-restrict: hide-start` / `# syft-restrict: hide-end` comments in the source.
13
+ - **Public** — every other line outside the marked ranges: imports, data loading, wrappers. It is
14
+ never checked or obfuscated. The recipient of the obfuscated file can read it directly.
15
+
16
+ Given that split, `syft-restrict` executes the following two steps:
17
+
18
+ - **The verifier** statically walks the private region and analyzes it against a default-deny
19
+ policy. Every construct must be explicitly allowed, or verification fails
20
+ and reports each offending line.
21
+ - **The obfuscator** runs after a clean verify. It creates an obfuscated copy by rewriting the private
22
+ region — renaming identifiers, blanking constants, replacing lines with `■■■■■■■■` — so the logic can't be read
23
+ back out, while the public region is copied through untouched.
24
+
25
+ The **verifier** is inspired by
26
+ [**RestrictedPython**](https://github.com/zopefoundation/RestrictedPython), but
27
+ differs in a few ways:
28
+
29
+ - **It analyzes — it doesn't run.** It never executes the source. Only the static checks decide pass/fail.
30
+ - The public code may **import allowlisted libraries** and call into them.
31
+ Imports are not allowed in private code, and any library call must be
32
+ **explicitly allow-listed**.
33
+ - **Dynamic Python lives in the public part.** Anything dynamic the author needs
34
+ (file/tokenizer loading, the generation loop, wrappers around library methods)
35
+ must be written in the **public** region — where it can be reviewed directly — and
36
+ may be **called by** the private part.
37
+
38
+ ## Usage
39
+
40
+ Mark the private region in the source with comments:
41
+
42
+ ```python
43
+ # syft-restrict: obfuscate-start
44
+ def attention(x):
45
+ ...
46
+ # syft-restrict: obfuscate-end
47
+
48
+ MODEL_ID = "gemma-2b" # syft-restrict: hide
49
+ ```
50
+
51
+ _See [docs/verify.md](docs/verify.md#public-vs-private) for the full marker syntax (single-line markers, nesting rules, and error cases)._
52
+
53
+ Then run:
54
+
55
+ ```python
56
+ import syft_restrict as restrict
57
+
58
+ result = restrict.run(
59
+ "gemma_inference.py",
60
+ allow_functions=["jax.*", "flax.linen.*"], # functions callable BY NAME (path-resolved)
61
+ allow_operators=["arithmetic", "indexing", "comparison"], # operators allowed ON A VALUE
62
+ )
63
+ # On success: writes gemma_inference.obfuscated.py and returns result.certificate.
64
+ # On a policy violation: raises PolicyViolation naming each offending line.
65
+ # If the file has no markers and obfuscate=/hide= are both omitted: raises MarkerError.
66
+ ```
67
+
68
+ `obfuscate=`/`hide=` still work as an explicit escape hatch — pass either one (even an
69
+ empty list) and marker scanning is skipped entirely in favor of your own 1-based line
70
+ ranges:
71
+
72
+ ```python
73
+ result = restrict.run(
74
+ "gemma_inference.py",
75
+ obfuscate=[[22, 93], [99, 280]], # 1-based ranges: identifiers renamed, constants blanked
76
+ hide=[], # 1-based ranges: whole line replaced with ■■■■■■■■
77
+ allow_functions=["jax.*", "flax.linen.*"],
78
+ allow_operators=["arithmetic", "indexing", "comparison"],
79
+ )
80
+ ```
81
+
82
+ This is how **[examples/gemma_inference.py](examples/gemma_inference.py)** generates
83
+ **[examples/gemma_inference.obfuscated.py](examples/gemma_inference.obfuscated.py)** —
84
+ see [examples/generate.py](examples/generate.py).
85
+
86
+ The same model marked up with comment blocks instead of numeric ranges lives in
87
+ **[examples/gemma_inference_marked.py](examples/gemma_inference_marked.py)** /
88
+ [examples/generate_marked.py](examples/generate_marked.py).
89
+
90
+ If syft-restrict was successfully executed on the true original file, the obfuscated file can be safely shared without exposing the private section.
91
+
92
+ Pass `strict=False` to `run` to get a `RunResult` with `.ok` / `.violations`
93
+ instead of an exception.
94
+
95
+ ## Documentation
96
+
97
+ - [docs/verify.md](docs/verify.md) — how verification works and what private code may do (allow side).
98
+ - [docs/blacklist.md](docs/blacklist.md) — default-deny catalog and violation codes (deny side).
99
+ - [docs/code-layout.md](docs/code-layout.md) — source modules and test layout.
@@ -0,0 +1,257 @@
1
+ # What is not allowed
2
+
3
+ This document lists everything the checker rejects in the **private** region.
4
+
5
+ - The model is **default-deny**. Tables below name common rejections with clear
6
+ codes.
7
+ - Anything not listed as allowed in [verify.md](verify.md) is also
8
+ rejected.
9
+ - Each entry includes the **violation code** returned by `verify()`.
10
+
11
+ ---
12
+
13
+ ## Index of violation codes
14
+
15
+ | Code | Meaning |
16
+ | ------------------- | ------------------------------------------------------------------------ |
17
+ | `node-type` | Syntax not on the allow list (walrus, `match`, …) |
18
+ | `banned-name` | Reference or call to a banned builtin (`open`, `eval`, …) |
19
+ | `banned-construct` | Forbidden statement/expression form (import, `with`, f-string, …) |
20
+ | `call-not-allowed` | Library path not allow-listed (or hit by `disallow_functions`) |
21
+ | `call-unresolved` | Call target not provably safe |
22
+ | `method-on-value` | Named method on an unknown value (`x.reshape`) |
23
+ | `attr-on-value` | Attribute read/write on an unknown value, or bad `self` chain |
24
+ | `attr-not-allowed` | Library attribute path not allow-listed |
25
+ | `dunder-attr` | Dunder attribute (`.__class__`, …) |
26
+ | `dunder-name` | Bare dunder name (`__class__`) |
27
+ | `dunder-def` | Defining a forbidden magic method |
28
+ | `class-keyword` | e.g. `metaclass=` |
29
+ | `class-base` | Base class not allow-listed |
30
+ | `reserved-name` | Rebinding a trusted name (`jnp`, public wrapper, private def, `self`, …) |
31
+ | `operator-disabled` | Operator used without enabling its group in `allow_operators` |
32
+
33
+ ---
34
+
35
+ ## Forbidden constructs
36
+
37
+ These constructs are banned outright in private code.
38
+
39
+ Code: **`banned-construct`**.
40
+
41
+ | Construct | Example | Why |
42
+ | --------------------- | ----------------------------------- | ------------------------------------------------------ |
43
+ | Import | `import os`, `from os import path` | Imports belong in public code |
44
+ | `with` | `with open(f) as g: ...` | Runs enter/exit hooks; often I/O |
45
+ | `try` / `raise` | `try: ... finally: ...` | Exception tricks / host surfaces |
46
+ | `global` / `nonlocal` | `global x` | Escape local naming rules |
47
+ | `del` | `del x` | Can remove names the policy relies on |
48
+ | `assert` | `assert cond` | Disappears under `python -O` |
49
+ | Async | `async def`, `await`, … | Out of scope for pure inference |
50
+ | Generators | `yield`, `yield from` | Suspended execution |
51
+ | F-strings | `f"hi"`, `f"{x}"` | Interpolation runs formatting with no normal call site |
52
+ | Decorators | `@property`, `@nn.compact`, `@evil` | Run code when the def/class is reached |
53
+
54
+ ```python
55
+ # rejected
56
+ import os
57
+ y = f"value={x}"
58
+ with ctx() as g:
59
+ pass
60
+
61
+ @nn.compact
62
+ def __call__(self, x): ...
63
+ ```
64
+
65
+ ### Unknown / future syntax
66
+
67
+ Any syntax not on the allow list is rejected.
68
+
69
+ Code: **`node-type`**.
70
+
71
+ | Example | Notes |
72
+ | ---------------------- | ---------------- |
73
+ | `y = (z := 1)` | Walrus |
74
+ | `match x: case 1: ...` | Pattern matching |
75
+
76
+ New Python syntax stays blocked until reviewed and explicitly allowed.
77
+
78
+ ---
79
+
80
+ ## Banned builtins
81
+
82
+ These names may never be **used** (i.e, loaded) in private code, even if they are
83
+ **not called**.
84
+
85
+ Code: **`banned-name`**.
86
+
87
+ ### Dynamic code / reflection / I/O
88
+
89
+ - `eval`
90
+ - `exec`
91
+ - `compile`
92
+ - `__import__`
93
+ - `getattr`
94
+ - `setattr`
95
+ - `delattr`
96
+ - `hasattr`
97
+ - `vars`
98
+ - `globals`
99
+ - `locals`
100
+ - `dir`
101
+ - `open`
102
+ - `input`
103
+ - `breakpoint`
104
+ - `memoryview`
105
+ - `type`
106
+ - `__build_class__`
107
+ - `print`
108
+
109
+ ### Formatting / buffer builtins
110
+
111
+ Same idea as calling dunders on a value (`x.__repr__()`), spelled as a bare call:
112
+
113
+ - `repr`
114
+ - `str`
115
+ - `ascii`
116
+ - `format`
117
+ - `bytes`
118
+
119
+ > [!WARNING] > `bytes(x)` can dump raw buffer contents. `print` is a stdout channel. F-strings are banned
120
+ > separately as constructs (above), including with no `{...}` at all.
121
+
122
+ ```python
123
+ # all rejected (banned-name)
124
+ f = open
125
+ d = {"o": open}
126
+ def run(op=open): ...
127
+ open("/etc/passwd") # call reports banned-name once
128
+ y = [v for v in open(path)] # passive positions still checked
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Calls and attributes
134
+
135
+ | What | Example | Code |
136
+ | -------------------------------------------- | ------------------------------------------ | ------------------ |
137
+ | Library path not allowed | `np.dot(a, b)` | `call-not-allowed` |
138
+ | Disallow list hits an otherwise-allowed path | `jnp.save(...)` under `disallow_functions` | `call-not-allowed` |
139
+ | Call target not proven safe | `fn(x)` where `fn` is a parameter | `call-unresolved` |
140
+ | Named method on a value | `x.reshape(8, -1)`, `items.append(1)` | `method-on-value` |
141
+ | Attribute on a value | `x.shape`, `x.T`, `obj.send = data` | `attr-on-value` |
142
+ | Deep `self` chain | `self.a.b`, `self.sub.evil(...)` | `attr-on-value` |
143
+ | Unsafe `self.<name>(...)` | stashed `open` on `self` in `setup` | `attr-on-value` |
144
+ | Dunder attribute | `obj.__class__`, `self.__dict__` | `dunder-attr` |
145
+ | Bare dunder name | `c = __class__` | `dunder-name` |
146
+ | Library attr not allowed | `np.pi` when numpy is not allowed | `attr-not-allowed` |
147
+
148
+ ```python
149
+ # rejected
150
+ a = x.reshape(8, -1) # method-on-value
151
+ b = x.shape # attr-on-value
152
+ c = obj.__class__ # dunder-attr
153
+
154
+ def apply(fn, x):
155
+ return fn(x) # call-unresolved
156
+
157
+ ```
158
+
159
+ > [!IMPORTANT] > **`call-unresolved` is default-deny for callees.** It catches dangerous callables that never
160
+ > mention a banned builtin by name (parameters, opaque locals, `d["k"](x)`).
161
+
162
+ Only single-level `self.<name>` / `cls.<name>` is special-cased. Rules for when `self.x(...)` is
163
+ safe are in [verify.md](verify.md#self-and-flax-style-modules).
164
+
165
+ ---
166
+
167
+ ## Classes and definitions
168
+
169
+ | What | Example | Code |
170
+ | ---------------------- | ------------------------------------- | --------------- |
171
+ | Class keywords | `class M(nn.Module, metaclass=Meta)` | `class-keyword` |
172
+ | Bad base class | `class M(SomeLib)`, `class M(object)` | `class-base` |
173
+ | Forbidden magic method | `def __getattr__`, `def __reduce__` | `dunder-def` |
174
+
175
+ Allowed hooks only: `setup`, `__call__`.
176
+
177
+ Decorators are banned outright as a [forbidden construct](#forbidden-constructs).
178
+
179
+ ```python
180
+ # rejected
181
+ class M(object): # class-base
182
+ def __getattr__(self, name): # dunder-def
183
+ return None
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Reserved names
189
+
190
+ Rebinding a name the checker trusts reports **`reserved-name`**.
191
+
192
+ | Reserved | Example rebind | Why |
193
+ | -------------------- | --------------------------------------------- | -------------------------------------- |
194
+ | Import aliases | `jnp = make_evil()` | Would lie about resolved library paths |
195
+ | Public wrappers | nested `def transpose` shadowing a public one | Defeats the reviewed wrapper |
196
+ | Private defs/classes | `helper = evil` after `def helper` | Bare calls trust the def by name |
197
+ | Safe builtins | `list = None` | Bare calls trust `list(...)` by name |
198
+ | `self` / `cls` | `self = x`, nested `def f(self):` | `self.*` is trusted by spelling |
199
+
200
+ Applies to assignment, `for` / comprehension targets, parameters, and nested defs.
201
+ Private def names are also protected against rebind in **public** glue between private ranges.
202
+
203
+ ```python
204
+ # rejected
205
+ import jax.numpy as jnp
206
+ jnp = 1 # reserved-name
207
+
208
+ def helper(x):
209
+ return x
210
+ def f(evil):
211
+ helper = evil # reserved-name
212
+ return helper(1)
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Operator bundles
218
+
219
+ If a group of operators is not in `allow_operators`, using any of its operators
220
+ reports **`operator-disabled`**.
221
+
222
+ | Bundle | If missing, these fail |
223
+ | ------------ | ---------------------- |
224
+ | `arithmetic` | `a + b`, `-a`, … |
225
+ | `comparison` | `a < b`, `a and b`, … |
226
+ | `indexing` | `x[0]`, `x[1:3]`, … |
227
+
228
+ ---
229
+
230
+ ## Optional `disallow_functions`
231
+
232
+ There is no built-in library denylist. Everything not explicitly allow-listed is
233
+ rejected. Safety comes from **`allow_functions`**.
234
+
235
+ When you use a broad allow (`jax.*`), pass `disallow_functions=[...]` for a hard floor. Hits report
236
+ **`call-not-allowed`**.
237
+
238
+ Useful patterns under broad JAX/Flax allows:
239
+
240
+ - Host / debug / experimental: `jax.experimental.*`, `jax.debug.*`, `jax.pure_callback`, `*.io_callback`, `*.host_callback*`
241
+ - FFI / interop: `jax.dlpack.*`, `jax.ffi*`
242
+ - Array ↔ disk: `jax.numpy.save`, `load`, `tofile`, `fromfile`, `memmap`, …
243
+ - Checkpointing: `flax.serialization.*`, `flax.training.checkpoints.*`, `orbax.*`
244
+
245
+ ```python
246
+ # public import, private use — still resolved and checked
247
+ from jax.numpy import save as persist
248
+ persist(x, "out.npz") # call-not-allowed if disallow includes jax.numpy.save
249
+ ```
250
+
251
+ ---
252
+
253
+ ## See also
254
+
255
+ - [verify.md](verify.md) — how checking works and what is allowed
256
+ - `tests/verify/test_disallowed.py` — disallowed tests
257
+ - `tests/verify/test_bypasses.py` — multi-step attack regressions
@@ -0,0 +1,23 @@
1
+ # Code layout
2
+
3
+ Map of `src/syft_restrict/`:
4
+
5
+ | Module | Role |
6
+ | --------------- | --------------------------------------------------------------------- |
7
+ | `astutil.py` | Line ranges, import scan, small syntax helpers. No policy. |
8
+ | `policy.py` | Allow lists, safe/banned builtins, hook list, `Policy`. |
9
+ | `verifier.py` | Static checker: walks private lines default-deny, reports violations. |
10
+ | `obfuscator.py` | After a clean verify: rename private identifiers, blank constants. |
11
+ | `runner.py` | `run()` — verify → obfuscate → certificate. |
12
+ | `errors.py` | `RestrictError`, `PolicyViolation`. |
13
+ | `__init__.py` | Public exports. |
14
+
15
+ Tests under `tests/verify/`:
16
+
17
+ | File | Role |
18
+ | ------------------------- | --------------------------------------------------------------- |
19
+ | `test_whitelist.py` | Green path (aligns with [verify.md](verify.md)) |
20
+ | `test_whitelisted_lib.py` | Library paths + operator bundles |
21
+ | `test_disallowed.py` | Default-deny catalog (aligns with [blacklist.md](blacklist.md)) |
22
+ | `test_bypasses.py` | Multi-step escape regressions |
23
+ | `test_ranges.py` | Invalid private ranges |