hasandmrkrn commited on
Commit
9e8e328
·
verified ·
1 Parent(s): 21b6f2c

Add Merlin V3 ungated native evaluation release

Browse files
Files changed (45) hide show
  1. README.md +16 -0
  2. v3-ungated/LICENSE +201 -0
  3. v3-ungated/NOTICE +7 -0
  4. v3-ungated/README.md +84 -0
  5. v3-ungated/evaluation.json +60 -0
  6. v3-ungated/native/cmd/merlin/main.go +52 -0
  7. v3-ungated/native/go.mod +8 -0
  8. v3-ungated/native/go.sum +4 -0
  9. v3-ungated/native/stepsafety/agreement.go +145 -0
  10. v3-ungated/native/stepsafety/context.go +247 -0
  11. v3-ungated/native/stepsafety/model/ATTRIBUTION.md +1 -0
  12. v3-ungated/native/stepsafety/model/LICENSE +21 -0
  13. v3-ungated/native/stepsafety/model/PROVENANCE.json +89 -0
  14. v3-ungated/native/stepsafety/model/native/PROVENANCE.json +83 -0
  15. v3-ungated/native/stepsafety/model/native/agreement.json.gz +3 -0
  16. v3-ungated/native/stepsafety/model/native/candidate.json +147 -0
  17. v3-ungated/native/stepsafety/model/native/manifest.json +49 -0
  18. v3-ungated/native/stepsafety/model/native/request_gate.json.gz +3 -0
  19. v3-ungated/native/stepsafety/model/native/tokenizer.json.gz +3 -0
  20. v3-ungated/native/stepsafety/model/native/weights.00.gz +3 -0
  21. v3-ungated/native/stepsafety/model/native/weights.01.gz +3 -0
  22. v3-ungated/native/stepsafety/model/native/weights.02.gz +3 -0
  23. v3-ungated/native/stepsafety/model/native/weights.03.gz +3 -0
  24. v3-ungated/native/stepsafety/model/native/weights.04.gz +3 -0
  25. v3-ungated/native/stepsafety/modelstore.go +296 -0
  26. v3-ungated/native/stepsafety/native_assets.go +15 -0
  27. v3-ungated/native/stepsafety/native_assets_nomodel.go +14 -0
  28. v3-ungated/native/stepsafety/native_backend.go +133 -0
  29. v3-ungated/native/stepsafety/native_linear_arm64.go +82 -0
  30. v3-ungated/native/stepsafety/native_linear_arm64.s +57 -0
  31. v3-ungated/native/stepsafety/native_linear_generic.go +20 -0
  32. v3-ungated/native/stepsafety/native_model.go +288 -0
  33. v3-ungated/native/stepsafety/native_weights.go +130 -0
  34. v3-ungated/native/stepsafety/native_workspace.go +31 -0
  35. v3-ungated/native/stepsafety/normalization.go +187 -0
  36. v3-ungated/native/stepsafety/packing.go +215 -0
  37. v3-ungated/native/stepsafety/request_gate.go +122 -0
  38. v3-ungated/native/stepsafety/runtime_artifacts.go +11 -0
  39. v3-ungated/native/stepsafety/runtime_store.go +204 -0
  40. v3-ungated/native/stepsafety/scoped.go +164 -0
  41. v3-ungated/native/stepsafety/tokenizer.go +122 -0
  42. v3-ungated/native/stepsafety/types.go +468 -0
  43. v3-ungated/native/stepsafety/unicode9.go +79 -0
  44. v3-ungated/native/stepsafety/unigram.go +167 -0
  45. v3-ungated/release_manifest.json +180 -0
README.md CHANGED
@@ -12,6 +12,22 @@ tags:
12
  - deberta-v3
13
  ---
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # Merlin
16
 
17
  Merlin is a 70.8M-parameter local encoder that classifies a proposed AI-agent
 
12
  - deberta-v3
13
  ---
14
 
15
+ <!-- merlin-v3-ungated-start -->
16
+ ## V3 ungated native evaluation release
17
+
18
+ The [V3 native Go package](v3-ungated/README.md) is available in `v3-ungated/`.
19
+ It keeps the V3 weights, threshold and action-agreement filter while removing the
20
+ three restrictive eligibility gates. AgentDojo recall is 24.1% (85/352), with
21
+ 20/868 benign cases flagged. See the package for coverage, private-data aggregate
22
+ results and limitations. No private evaluation examples are included.
23
+
24
+ The root checkpoint and Python usage below remain **legacy v0.1**, with their
25
+ original results. They are not the V3 runtime. This is a controlled evaluation
26
+ release, not a claim of uniform performance across deployments.
27
+ <!-- merlin-v3-ungated-end -->
28
+
29
+
30
+
31
  # Merlin
32
 
33
  Merlin is a 70.8M-parameter local encoder that classifies a proposed AI-agent
v3-ungated/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
v3-ungated/NOTICE ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Merlin
2
+ Copyright 2026 Kontext Security
3
+
4
+ The released checkpoint is fine-tuned from microsoft/deberta-v3-xsmall, which is
5
+ distributed under the MIT License. TS-Bench and its component datasets retain
6
+ their upstream terms. See DATASETS.md and the Hugging Face model card.
7
+
v3-ungated/README.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: merlin-research-release
4
+ base_model: microsoft/deberta-v3-xsmall
5
+ tags:
6
+ - agents
7
+ - agent-safety
8
+ - tool-use
9
+ - native-go
10
+ ---
11
+
12
+ # Merlin V3 ungated — local evaluation release
13
+
14
+ Runtime `merlin-v3-ungated-20261104` uses the unchanged V3 contextual encoder and
15
+ sparse action-agreement model from `merlin-v3-precision-20261007`. This is a
16
+ runtime eligibility change, not newly trained weights. It removes the operation
17
+ allowlist, lexical request gate and learned informational-request gate.
18
+
19
+ The threshold remains **0.5**. The complete current action must fit **128 tokens**;
20
+ long actions and excluded tools remain unassessed. The output score is a
21
+ threshold-relative agreement score, **not a calibrated probability of risk**.
22
+ This is an advisory detector; it does not execute or block tool calls.
23
+
24
+ ## Run locally
25
+
26
+ Download this folder (`v3-ungated`) from `kontext-security/Merlin`, preserving its
27
+ layout. Go 1.26.10 or later is required to build the standalone native runtime.
28
+ It embeds the supplied assets and needs no Python, ONNX runtime or hosted inference.
29
+ Go downloads normal build dependencies on the first build.
30
+
31
+ ```bash
32
+ hf download kontext-security/Merlin --include 'v3-ungated/*' --local-dir dist/Merlin
33
+ cd dist/Merlin/v3-ungated/native
34
+ go build -o merlin ./cmd/merlin
35
+ printf '%s\n' '{"id":"example","input":{"user_request":"Find public documentation","tool_name":"WebSearch","tool_arguments":{"query":"Go JSON decoder documentation"},"interaction_history":"[]"}}' | ./merlin
36
+ ```
37
+
38
+ Each input line contains `id` and `input`. Input fields are `user_request`,
39
+ `interaction_history` (a JSON array encoded as a string), `tool_name`,
40
+ `tool_arguments`, and optionally `available_tool_schemas`. Include only completed
41
+ **prior** tool observations in history, never the current result or later turns.
42
+ Each output line preserves `id` and includes `evaluation`; unavailable assessments
43
+ have an `error_code` and no score. Do not treat those as safe classifications.
44
+
45
+ ## Evaluation
46
+
47
+ | Cohort | Assessed / available | Detections or alerts |
48
+ |---|---:|---|
49
+ | AgentDojo-Traj | 1,159 / 1,220 | 85/352 positives detected (24.1% recall); 20/868 benign cases flagged (2.3% FPR) |
50
+ | Private historical regression | 415 / 610 | 1 false alert |
51
+ | Private later workload sample | 600 / 1,207 | 3 clear false alerts, 1 uncertain low-impact alert |
52
+
53
+ The original collected false-positive subset has 539 saved cases, of which 409
54
+ were reconstructed and assessed: Claude Code 0/128 alerts; Codex 1/281. The expanded
55
+ historical panel above includes these cases. These cohorts are not additive.
56
+ The later sample's clear false-alert frequency is 3/600 assessed calls (0.50%);
57
+ 607 cases remain unassessed. This is not a population false-positive guarantee.
58
+
59
+ Historical data overlap training/selection/calibration and AgentDojo was repeatedly
60
+ consulted during development. The later sample is stratified, not traffic weighted.
61
+ Historical context was reconstructed from transcripts rather than exact daemon
62
+ snapshots. The remaining historical examples are unresolved, not successful tests.
63
+ No fresh ungated ASB or AgentHarm result is claimed. The earlier v0.1 benchmark
64
+ numbers do not describe this runtime.
65
+
66
+ This release is suitable for controlled evaluation. It is not evidence of uniform
67
+ precision on unfamiliar tools or workloads. See `evaluation.json` for aggregates
68
+ and `release_manifest.json` for file checksums. **No private prompts, calls,
69
+ transcripts, reviewer text, identifiers or per-example scores are distributed.**
70
+ Private evaluation requires the internal access-controlled archive; the public
71
+ release cannot independently reproduce its private-data metrics.
72
+
73
+ ## Provenance and terms
74
+
75
+ Base encoder: `microsoft/deberta-v3-xsmall` (MIT), revision
76
+ `4b419818330868dff6a60ad3e6b1c730f8b8c0c6`. The V3 encoder weights SHA-256 is
77
+ `429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1`.
78
+ V3 was developed using benchmark data and private training/selection examples;
79
+ only model parameters and aggregate evaluation results are released here.
80
+ Upstream dataset/model terms continue to apply; consult the bundled attribution
81
+ and provenance files. The checkpoint is a research release (`other` license),
82
+ not a blanket commercial-use license. Runtime source follows the repository
83
+ Apache-2.0 license. The older request-intent asset remains for provenance but is
84
+ not used by the ungated inference path.
v3-ungated/evaluation.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "runtime": "merlin-v3-ungated-20261104",
3
+ "weights_candidate": "merlin-v3-precision-20261007",
4
+ "threshold": 0.5,
5
+ "action_token_limit": 128,
6
+ "action_agreement": true,
7
+ "removed_gates": [
8
+ "operation_allowlist",
9
+ "lexical_request",
10
+ "learned_request_intent"
11
+ ],
12
+ "AgentDojo": {
13
+ "total": 1220,
14
+ "assessed": 1159,
15
+ "tp": 85,
16
+ "fp": 20,
17
+ "positives": 352,
18
+ "negatives": 868
19
+ },
20
+ "private_historical_regression": {
21
+ "total": 415,
22
+ "assessed": 415,
23
+ "skipped": 0,
24
+ "alerts": 1
25
+ },
26
+ "private_original_flagged_no_risk": {
27
+ "saved": 539,
28
+ "reconstructed_and_assessed": 409,
29
+ "alerts": 1,
30
+ "claude_code": {
31
+ "saved": 133,
32
+ "assessed": 128,
33
+ "alerts": 0
34
+ },
35
+ "codex": {
36
+ "saved": 406,
37
+ "assessed": 281,
38
+ "alerts": 1
39
+ }
40
+ },
41
+ "private_recent_sample": {
42
+ "total": 1207,
43
+ "assessed": 600,
44
+ "skipped": 607,
45
+ "alerts": 4
46
+ },
47
+ "private_recent_alert_review": {
48
+ "clear_false_alerts": 3,
49
+ "uncertain_low_impact": 1,
50
+ "confirmed_true_positive": 0
51
+ },
52
+ "limitations": [
53
+ "Historical examples overlap model development; regression evidence, not unseen holdout.",
54
+ "AgentDojo was repeatedly consulted during development.",
55
+ "Recent sample is stratified and reconstructed from pre-call transcripts; not representative of every deployment.",
56
+ "Unassessed examples are not safe predictions.",
57
+ "No fresh ungated ASB or AgentHarm result is claimed."
58
+ ],
59
+ "private_examples_published": false
60
+ }
v3-ungated/native/cmd/merlin/main.go ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "time"
10
+
11
+ "github.com/kontext-security/merlin/native/stepsafety"
12
+ )
13
+
14
+ // Merlin only evaluates these inputs; it never executes the supplied tool calls.
15
+ func main() {
16
+ ctx := context.Background()
17
+ evaluator := stepsafety.New(ctx, stepsafety.Config{Enabled: true, Timeout: 30 * time.Second, StartupTimeout: 30 * time.Second, MaxConcurrency: 1})
18
+ health := evaluator.Health(ctx)
19
+ if health.Status != "ready" {
20
+ fmt.Fprintln(os.Stderr, "Merlin initialization failed:", health.ErrorCode)
21
+ os.Exit(1)
22
+ }
23
+ scan := bufio.NewScanner(os.Stdin)
24
+ scan.Buffer(make([]byte, 65536), 4*1024*1024)
25
+ output := json.NewEncoder(os.Stdout)
26
+ for scan.Scan() {
27
+ var row struct {
28
+ ID string `json:"id"`
29
+ Input stepsafety.Input `json:"input"`
30
+ }
31
+ if err := json.Unmarshal(scan.Bytes(), &row); err != nil {
32
+ fmt.Fprintln(os.Stderr, "Invalid input JSON:", err)
33
+ os.Exit(1)
34
+ }
35
+ if row.Input.ToolName == "" {
36
+ fmt.Fprintln(os.Stderr, "input.tool_name is required")
37
+ os.Exit(1)
38
+ }
39
+ result := struct {
40
+ ID string `json:"id"`
41
+ Evaluation stepsafety.Evaluation `json:"evaluation"`
42
+ }{row.ID, evaluator.Evaluate(ctx, row.Input)}
43
+ if err := output.Encode(result); err != nil {
44
+ fmt.Fprintln(os.Stderr, err)
45
+ os.Exit(1)
46
+ }
47
+ }
48
+ if err := scan.Err(); err != nil {
49
+ fmt.Fprintln(os.Stderr, err)
50
+ os.Exit(1)
51
+ }
52
+ }
v3-ungated/native/go.mod ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ module github.com/kontext-security/merlin/native
2
+
3
+ go 1.26.10
4
+
5
+ require (
6
+ golang.org/x/text v0.43.0
7
+ gonum.org/v1/gonum v0.17.0
8
+ )
v3-ungated/native/go.sum ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
2
+ golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
3
+ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
4
+ gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
v3-ungated/native/stepsafety/agreement.go ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "compress/gzip"
5
+ "context"
6
+ "crypto/sha256"
7
+ "encoding/hex"
8
+ "encoding/json"
9
+ "errors"
10
+ "golang.org/x/text/cases"
11
+ "golang.org/x/text/language"
12
+ "io"
13
+ "math"
14
+ "strings"
15
+ "unicode"
16
+ )
17
+
18
+ type sparseVector struct {
19
+ Features map[string][2]float64 `json:"features"`
20
+ }
21
+ type scoreCalibration struct{ Coefficient, Intercept float64 }
22
+
23
+ var agreementModel struct {
24
+ Word, Char sparseVector
25
+ Intercept float64
26
+ EncoderCalibration scoreCalibration `json:"encoder_calibrator"`
27
+ SparseCalibration scoreCalibration `json:"sparse_calibrator"`
28
+ EncoderThreshold float64 `json:"encoder_threshold"`
29
+ SparseThreshold float64 `json:"sparse_threshold"`
30
+ }
31
+
32
+ func loadAgreementAssets() error {
33
+ f, err := nativeAssets.Open("model/native/agreement.json.gz")
34
+ if err != nil {
35
+ return err
36
+ }
37
+ defer f.Close()
38
+ reader, err := gzip.NewReader(f)
39
+ if err != nil {
40
+ return err
41
+ }
42
+ defer reader.Close()
43
+ data, err := io.ReadAll(io.LimitReader(reader, 4918863+1))
44
+ if err != nil {
45
+ return err
46
+ }
47
+ h := sha256.Sum256(data)
48
+ if len(data) != 4918863 || hex.EncodeToString(h[:]) != "7f01698e14f36f72ccad7f519c4e95298f125c75c469ef25b4b0f08052f1dcc8" {
49
+ return errors.New("agreement asset checksum mismatch")
50
+ }
51
+ return json.Unmarshal(data, &agreementModel)
52
+ }
53
+
54
+ func vectorScore(counts map[string]int, vector sparseVector) float64 {
55
+ var norm, dot float64
56
+ for term, count := range counts {
57
+ h := sha256.Sum256([]byte(term))
58
+ feature, ok := vector.Features[hex.EncodeToString(h[:])]
59
+ if !ok {
60
+ continue
61
+ }
62
+ value := (1 + math.Log(float64(count))) * feature[0]
63
+ norm += value * value
64
+ dot += value * feature[1]
65
+ }
66
+ if norm == 0 {
67
+ return 0
68
+ }
69
+ return dot / math.Sqrt(norm)
70
+ }
71
+
72
+ func pythonSpace(r rune) bool { return unicode.IsSpace(r) || (r >= 0x1c && r <= 0x1f) }
73
+
74
+ func sparseMargin(text string) float64 {
75
+ // Match sklearn's lowercase Unicode word analyzer and char analyzer.
76
+ // The vocabulary is hashed to avoid shipping plaintext captured tokens.
77
+ text = cases.Lower(language.Und).String(text)
78
+ words := []string{}
79
+ run := []rune{}
80
+ flush := func() {
81
+ if len(run) >= 2 {
82
+ words = append(words, string(run))
83
+ }
84
+ run = run[:0]
85
+ }
86
+ for _, r := range text {
87
+ if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
88
+ run = append(run, r)
89
+ } else {
90
+ flush()
91
+ }
92
+ }
93
+ flush()
94
+ counts := map[string]int{}
95
+ for i, word := range words {
96
+ counts[word]++
97
+ if i > 0 {
98
+ counts[words[i-1]+" "+word]++
99
+ }
100
+ }
101
+ score := agreementModel.Intercept + vectorScore(counts, agreementModel.Word)
102
+ var normalized strings.Builder
103
+ rr := []rune(text)
104
+ for i := 0; i < len(rr); {
105
+ j := i + 1
106
+ if pythonSpace(rr[i]) {
107
+ for j < len(rr) && pythonSpace(rr[j]) {
108
+ j++
109
+ }
110
+ }
111
+ if j-i >= 2 {
112
+ normalized.WriteByte(' ')
113
+ } else {
114
+ normalized.WriteRune(rr[i])
115
+ }
116
+ i = j
117
+ }
118
+ rr = []rune(normalized.String())
119
+ counts = map[string]int{}
120
+ for n := 3; n <= 5; n++ {
121
+ for i := 0; i+n <= len(rr); i++ {
122
+ counts[string(rr[i:i+n])]++
123
+ }
124
+ }
125
+ return score + vectorScore(counts, agreementModel.Char)
126
+ }
127
+
128
+ func applyAgreement(ctx context.Context, input Input, original InferenceResult) (InferenceResult, error) {
129
+ if err := ctx.Err(); err != nil {
130
+ return InferenceResult{}, err
131
+ }
132
+ fields, err := candidateFields(input)
133
+ if err != nil {
134
+ return InferenceResult{}, err
135
+ }
136
+ e, s := agreementModel.EncoderCalibration, agreementModel.SparseCalibration
137
+ et, st := agreementModel.EncoderThreshold, agreementModel.SparseThreshold
138
+ encoder := e.Coefficient*(original.Logits[1]-original.Logits[0]) + e.Intercept - math.Log(et/(1-et))
139
+ sparse := s.Coefficient*sparseMargin(fields[2]) + s.Intercept - math.Log(st/(1-st))
140
+ if err := ctx.Err(); err != nil {
141
+ return InferenceResult{}, err
142
+ }
143
+ original.Logits = [2]float64{0, math.Min(encoder, sparse)}
144
+ return original, nil
145
+ }
v3-ungated/native/stepsafety/context.go ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "strings"
7
+ "sync"
8
+ )
9
+
10
+ const (
11
+ maxContextSessions = 256
12
+ maxHistoryEntries = 24
13
+ maxHistoryBytes = maxPretokenizedFieldBytes
14
+ // Retain results that fit the history field so token packing can keep their
15
+ // beginning and end. The total per-session bound remains maxHistoryBytes.
16
+ maxHistoryEntryBytes = maxHistoryBytes
17
+ maxUserRequestBytes = maxPretokenizedFieldBytes
18
+ )
19
+
20
+ type HistoryEntry struct {
21
+ ToolName string
22
+ ToolArguments map[string]any
23
+ ToolResponse map[string]any
24
+ Error string
25
+ }
26
+
27
+ type sessionContext struct {
28
+ request string
29
+ history []string
30
+ historyOmitted bool
31
+ requestTooLarge bool
32
+ serial uint64
33
+ }
34
+
35
+ // ContextStore keeps only production-visible, structured hook data. It never
36
+ // reads transcripts or assistant messages, so agent Thought/reasoning cannot
37
+ // enter the model through this history path. The store is memory-only and
38
+ // bounded; raw context is never part of telemetry.
39
+ type ContextStore struct {
40
+ mu sync.Mutex
41
+ sessions map[string]*sessionContext
42
+ serial uint64
43
+ }
44
+
45
+ func NewContextStore() *ContextStore {
46
+ return &ContextStore{sessions: make(map[string]*sessionContext)}
47
+ }
48
+
49
+ func (s *ContextStore) RecordUserRequest(sessionID, request string) {
50
+ if s == nil || sessionID == "" {
51
+ return
52
+ }
53
+ s.mu.Lock()
54
+ defer s.mu.Unlock()
55
+ ctx := s.session(sessionID)
56
+ ctx.requestTooLarge = len(request) > maxUserRequestBytes
57
+ if ctx.requestTooLarge {
58
+ ctx.request = ""
59
+ } else {
60
+ ctx.request = strings.Clone(request)
61
+ }
62
+ }
63
+
64
+ func (s *ContextStore) RecordInteraction(sessionID string, entry HistoryEntry) {
65
+ if s == nil || sessionID == "" || entry.ToolName == "" {
66
+ return
67
+ }
68
+ s.mu.Lock()
69
+ defer s.mu.Unlock()
70
+ ctx := s.session(sessionID)
71
+ if ExcludedTool(entry.ToolName) {
72
+ ctx.historyOmitted = true
73
+ return
74
+ }
75
+ remaining := maxHistoryEntryBytes
76
+ for _, value := range []any{entry.ToolName, entry.ToolArguments, entry.ToolResponse, entry.Error} {
77
+ if err := boundedJSONSize(value, &remaining, 0); err != nil {
78
+ ctx.historyOmitted = true
79
+ return
80
+ }
81
+ }
82
+ encoded, err := serializeHistory([]HistoryEntry{entry})
83
+ if err != nil || len(encoded) > maxHistoryEntryBytes {
84
+ ctx.historyOmitted = true
85
+ return
86
+ }
87
+ // Store an immutable, bounded serialization, never references to caller
88
+ // maps or large raw tool results. Drop large events before evicting history.
89
+ ctx.history = append(ctx.history, encoded[1:len(encoded)-1])
90
+ for len(ctx.history) > maxHistoryEntries || len(joinHistory(ctx.history)) > maxHistoryBytes {
91
+ ctx.history = append([]string(nil), ctx.history[1:]...)
92
+ ctx.historyOmitted = true
93
+ }
94
+ }
95
+
96
+ func joinHistory(entries []string) string { return "[" + strings.Join(entries, ",") + "]" }
97
+
98
+ type ContextSnapshot struct {
99
+ UserRequest string
100
+ InteractionHistory string
101
+ HistoryOmitted bool
102
+ RequestTooLarge bool
103
+ }
104
+
105
+ func (s *ContextStore) Snapshot(sessionID string) (userRequest, interactionHistory string) {
106
+ snapshot := s.SnapshotWithCoverage(sessionID)
107
+ return snapshot.UserRequest, snapshot.InteractionHistory
108
+ }
109
+
110
+ func (s *ContextStore) SnapshotWithCoverage(sessionID string) ContextSnapshot {
111
+ empty := ContextSnapshot{InteractionHistory: "[]"}
112
+ if s == nil || sessionID == "" {
113
+ return empty
114
+ }
115
+ s.mu.Lock()
116
+ defer s.mu.Unlock()
117
+ ctx := s.sessions[sessionID]
118
+ if ctx == nil {
119
+ return empty
120
+ }
121
+ s.serial++
122
+ ctx.serial = s.serial
123
+ return ContextSnapshot{
124
+ UserRequest: ctx.request, InteractionHistory: joinHistory(ctx.history),
125
+ HistoryOmitted: ctx.historyOmitted, RequestTooLarge: ctx.requestTooLarge,
126
+ }
127
+ }
128
+
129
+ // serializeHistory implements the exact structured-history representation used
130
+ // to train the v2 encoder: a compact, key-sorted JSON array with tool,
131
+ // arguments, and string observation fields. Empty history is always [].
132
+ // Tool results remain inert text so their contents can never become a trusted
133
+ // instruction channel through a richer runtime type.
134
+ func serializeHistory(entries []HistoryEntry) (string, error) {
135
+ events := make([]map[string]any, 0, len(entries))
136
+ for _, entry := range entries {
137
+ if strings.TrimSpace(entry.ToolName) == "" {
138
+ continue
139
+ }
140
+ event := map[string]any{"tool": entry.ToolName}
141
+ if entry.ToolArguments != nil {
142
+ event["arguments"] = entry.ToolArguments
143
+ }
144
+ if observation, ok, err := observationText(entry.ToolResponse, entry.Error); err != nil {
145
+ return "", err
146
+ } else if ok {
147
+ event["observation"] = observation
148
+ }
149
+ events = append(events, event)
150
+ }
151
+ return compactSortedJSON(events)
152
+ }
153
+
154
+ func observationText(response map[string]any, errorText string) (string, bool, error) {
155
+ if response == nil {
156
+ if errorText == "" {
157
+ return "", false, nil
158
+ }
159
+ return errorText, true, nil
160
+ }
161
+ if errorText == "" {
162
+ encoded, err := compactSortedJSON(response)
163
+ return encoded, true, err
164
+ }
165
+ encoded, err := compactSortedJSON(map[string]any{
166
+ "error": errorText,
167
+ "response": response,
168
+ })
169
+ return encoded, true, err
170
+ }
171
+
172
+ // compactSortedJSON matches Python json.dumps(..., ensure_ascii=False,
173
+ // sort_keys=True, separators=(",", ":")) for the JSON values admitted by the
174
+ // hook adapters. encoding/json sorts object keys; disabling HTML escaping keeps
175
+ // ordinary Unicode and symbols byte-compatible with the training proxy.
176
+ func compactSortedJSON(value any) (string, error) {
177
+ var output bytes.Buffer
178
+ encoder := json.NewEncoder(&output)
179
+ encoder.SetEscapeHTML(false)
180
+ if err := encoder.Encode(value); err != nil {
181
+ return "", err
182
+ }
183
+ return unescapeJSONLineSeparators(strings.TrimSuffix(output.String(), "\n")), nil
184
+ }
185
+
186
+ // encoding/json always escapes U+2028 and U+2029 for legacy JSONP safety,
187
+ // while Python ensure_ascii=False keeps them as UTF-8. Only decode genuine JSON
188
+ // escapes here; an input string containing the literal text `\u2028` must keep
189
+ // its leading backslash.
190
+ func unescapeJSONLineSeparators(value string) string {
191
+ if !strings.Contains(value, `\u2028`) && !strings.Contains(value, `\u2029`) {
192
+ return value
193
+ }
194
+ var output strings.Builder
195
+ output.Grow(len(value))
196
+ for index := 0; index < len(value); {
197
+ if index+6 <= len(value) && (value[index:index+6] == `\u2028` || value[index:index+6] == `\u2029`) {
198
+ precedingSlashes := 0
199
+ for prior := index - 1; prior >= 0 && value[prior] == '\\'; prior-- {
200
+ precedingSlashes++
201
+ }
202
+ if precedingSlashes%2 == 0 {
203
+ if value[index+5] == '8' {
204
+ output.WriteRune('\u2028')
205
+ } else {
206
+ output.WriteRune('\u2029')
207
+ }
208
+ index += 6
209
+ continue
210
+ }
211
+ }
212
+ output.WriteByte(value[index])
213
+ index++
214
+ }
215
+ return output.String()
216
+ }
217
+
218
+ func (s *ContextStore) CloseSession(sessionID string) {
219
+ if s == nil || sessionID == "" {
220
+ return
221
+ }
222
+ s.mu.Lock()
223
+ defer s.mu.Unlock()
224
+ delete(s.sessions, sessionID)
225
+ }
226
+
227
+ func (s *ContextStore) session(sessionID string) *sessionContext {
228
+ if existing := s.sessions[sessionID]; existing != nil {
229
+ s.serial++
230
+ existing.serial = s.serial
231
+ return existing
232
+ }
233
+ if len(s.sessions) >= maxContextSessions {
234
+ var oldestID string
235
+ var oldest uint64
236
+ for id, candidate := range s.sessions {
237
+ if oldestID == "" || candidate.serial < oldest {
238
+ oldestID, oldest = id, candidate.serial
239
+ }
240
+ }
241
+ delete(s.sessions, oldestID)
242
+ }
243
+ s.serial++
244
+ created := &sessionContext{serial: s.serial}
245
+ s.sessions[sessionID] = created
246
+ return created
247
+ }
v3-ungated/native/stepsafety/model/ATTRIBUTION.md ADDED
@@ -0,0 +1 @@
 
 
1
+ The contextual DeBERTa-v3-xsmall base is Microsoft MIT-licensed (see LICENSE in this directory). Kontext's fine-tuned checkpoint is combined with two independently trained sparse heads. The request-intent head is adapted from MASSIVE 1.1 en-US, Copyright Amazon.com Inc. or its affiliates, derived from SLURP, licensed CC BY 4.0: https://github.com/alexa/massive and https://creativecommons.org/licenses/by/4.0/ . Adaptations include a binary query-intent mapping, deduplication, TF-IDF feature extraction and fitted logistic weights. No raw MASSIVE utterances or local captures are included in this serving bundle.
v3-ungated/native/stepsafety/model/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
v3-ungated/native/stepsafety/model/PROVENANCE.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 3,
3
+ "model_version": "toolsafe-deberta-v3-xsmall-onnx-scoped-v2",
4
+ "source_project": "kontext-security/toolsafe-lab",
5
+ "source_revision": "9c63e6191598b0ba72947a4394ac8297c41053d1",
6
+ "source_artifact_path": "artifacts/models/history_serialization/deberta_v3_xsmall",
7
+ "source_results_path": "results/history_serialization_ablation.json",
8
+ "source_results_sha256": "09efd898c02dedb1ad8f83dd5b90888e84cbcf8919c51450c9289176c42db41a",
9
+ "source_findings_path": "docs/HISTORY_SERIALIZATION_ABLATION_FINDINGS.md",
10
+ "source_findings_sha256": "f4231362a77e8d622ace51c4d8ac323fc801cca6ebc6771d5295b55df2bef08f",
11
+ "source_serializer_path": "src/toolsafe_lab/history_serialization.py",
12
+ "source_serializer_sha256": "4b14a72a6825555d56c4be0549039428b78332940d3cdd25516ef278888642e2",
13
+ "source_packing_path": "src/toolsafe_lab/standalone_encoder.py",
14
+ "source_packing_sha256": "ca8db9401025c8489a142d9447140a857fd77df84e5246fde92c995ccbe173ff",
15
+ "source_protocol_path": "docs/HISTORY_SERIALIZATION_ABLATION_PROTOCOL.md",
16
+ "source_protocol_sha256": "6e44e1175be44573e6feef1130ef9f4faeaf8457025c79d5100212c568099089",
17
+ "base_model": "microsoft/deberta-v3-xsmall",
18
+ "base_model_revision": "4b419818330868dff6a60ad3e6b1c730f8b8c0c6",
19
+ "base_model_license": "MIT",
20
+ "input_mode": "execution_context + canonical structured history (agent Thought excluded)",
21
+ "thought_included": false,
22
+ "history_serialization": {
23
+ "event_fields": [
24
+ "tool",
25
+ "arguments",
26
+ "observation"
27
+ ],
28
+ "empty_history": "[]",
29
+ "observations": "strings",
30
+ "json_encoding": "UTF-8, sorted keys, compact separators"
31
+ },
32
+ "evaluation": {
33
+ "threshold": 0.5,
34
+ "accuracy": 0.9118629908103593,
35
+ "precision": 0.9266195885784464,
36
+ "recall": 0.8845252051582649,
37
+ "f1": 0.9050832208726945,
38
+ "mcc": 0.8236797425872459,
39
+ "worst_source_recall": 0.5085227272727273
40
+ },
41
+ "max_length": 512,
42
+ "field_markers": {
43
+ "request": "[USER_REQUEST]",
44
+ "history": "[INTERACTION_HISTORY]",
45
+ "action": "[CURRENT_ACTION]",
46
+ "schema": "[TOOL_DESCRIPTIONS]"
47
+ },
48
+ "field_budgets": {
49
+ "request": 96,
50
+ "history": 144,
51
+ "action": 128,
52
+ "schema": 128
53
+ },
54
+ "calibration_kind": "sigmoid",
55
+ "calibration_scale": 1.427213430140093,
56
+ "calibration_bias": 2.953687013257505,
57
+ "initial_threshold": 0.5,
58
+ "artifacts": [
59
+ {
60
+ "name": "config.json",
61
+ "size_bytes": 924,
62
+ "sha256": "41f89ae28c4f0549b92537d0ef5918ef9e1af25e9898af6456e3551bf0a93852"
63
+ },
64
+ {
65
+ "name": "model.onnx",
66
+ "size_bytes": 287233599,
67
+ "sha256": "7f7c068c80e826cee69fd3fca089662572b3372b0be109c6cf17b6852d94557c"
68
+ },
69
+ {
70
+ "name": "tokenizer.json",
71
+ "size_bytes": 8340654,
72
+ "sha256": "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031"
73
+ },
74
+ {
75
+ "name": "tokenizer_config.json",
76
+ "size_bytes": 601,
77
+ "sha256": "141468772d3b095cc30275fd9c05dff92a78619c341487da9f5549a3564ea6cb"
78
+ }
79
+ ],
80
+ "excluded_training_artifact": {
81
+ "name": "training_checkpoint.pt",
82
+ "reason": "optimizer/RNG training-resume state is not required for inference"
83
+ },
84
+ "runtime_format": "ONNX opset 17, float32, [1,512] int64 input_ids/attention_mask, [1,2] logits",
85
+ "input_policy": "scoped-v2: exclude file tools; whole request/action/schema or abstain; filtered history uses first 72 and last 72 tokens, matching training",
86
+ "source_weights_sha256": "075d68f8bb4c1b4b98e2d2b2c0d5b013056b202172b080b34e7364b698844c86",
87
+ "export_toolchain": "torch 2.13.0; transformers 5.14.1; onnx 1.22.0; legacy exporter; scripts/step-safety/export_onnx.py",
88
+ "evaluation_scope": "Original held-out TS-Bench checkpoint evaluation; not an estimate for scoped-v2 production coverage"
89
+ }
v3-ungated/native/stepsafety/model/native/PROVENANCE.json ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_version": "merlin-v3-ungated-20261104",
3
+ "source_weights_sha256": "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1",
4
+ "tokenizer_sha256": "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031",
5
+ "experiment": "exp-20261007-merlin-v3-precision",
6
+ "candidate": "semantic_conservative",
7
+ "runtime": "Native Go FP32 DeBERTa plus native sparse TF-IDF logistic veto; all assets embedded, no ONNX or Python needed",
8
+ "base_model": "microsoft/deberta-v3-xsmall",
9
+ "base_revision": "4b419818330868dff6a60ad3e6b1c730f8b8c0c6",
10
+ "base_license": "MIT",
11
+ "scope": "All otherwise eligible tool invocations; operation allowlist, lexical authorization gate and learned request-intent gate removed",
12
+ "score_semantics": "Agreement margin rescaled to [0,1], not a calibrated production risk probability",
13
+ "context_budgets": [
14
+ 96,
15
+ 144,
16
+ 128,
17
+ 128
18
+ ],
19
+ "context_truncation": "Request head; history/schema head-tail; current action never truncated",
20
+ "advisory_only": true,
21
+ "deployed": false,
22
+ "sparse_model_sha256": "cddc99dd145eeb4e29ebfb35bc874408c2737b4f340917919024dc7e9113c229",
23
+ "selection_sha256": "e682888797fb0f43bffffe0b31d196aa82be6195e65ad74cc52e61043eaa1b28",
24
+ "evaluation_sha256": "b2f7d0ee87a9bf30a5aeb80768401ade17df613e6de1befd764b5052160ee7a0",
25
+ "exporter_sha256": "a1847a55de609edfb18dbfab0ad00e863c9d10d46fa7e37127fb8724b69e2d64",
26
+ "request_gate": {
27
+ "model_sha256": "0ec73c392582b7f93a6c59c78e3153cd36be98d36cab73445f0f300dcb087528",
28
+ "threshold": 0.7871534193379106,
29
+ "training_source": "MASSIVE 1.1 en-US original train, CC BY 4.0",
30
+ "selection_sha256": "ab818785fe65d6207673ebb8841954928ec692d27d04d1077175d7de8a2899e1",
31
+ "calibrator": {
32
+ "coefficient": 0.937442667480181,
33
+ "intercept": 0.020245760734966128
34
+ },
35
+ "enabled": false
36
+ },
37
+ "authorization_additional_words": [
38
+ "adjust",
39
+ "approve",
40
+ "archive",
41
+ "buy",
42
+ "checkout",
43
+ "clear",
44
+ "commit",
45
+ "disburse",
46
+ "discard",
47
+ "dispose",
48
+ "drop",
49
+ "empty",
50
+ "forward",
51
+ "grant",
52
+ "invite",
53
+ "merge",
54
+ "move",
55
+ "order",
56
+ "post",
57
+ "publish",
58
+ "purchase",
59
+ "purge",
60
+ "push",
61
+ "refund",
62
+ "reimburse",
63
+ "rename",
64
+ "reply",
65
+ "reschedule",
66
+ "respond",
67
+ "restart",
68
+ "rotate",
69
+ "save",
70
+ "set",
71
+ "share",
72
+ "start",
73
+ "stop",
74
+ "submit",
75
+ "unlink",
76
+ "upload",
77
+ "wipe"
78
+ ],
79
+ "runtime_variant": "v3-ungated-20261104",
80
+ "weights_candidate": "merlin-v3-precision-20261007",
81
+ "authorization_lexical_gate_enabled": false,
82
+ "operation_gate_enabled": false
83
+ }
v3-ungated/native/stepsafety/model/native/agreement.json.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6ffbd35e56a031df9f2ca08f7520486cc8ef65592c8d03cfe966fbd42523bbdf
3
+ size 2290715
v3-ungated/native/stepsafety/model/native/candidate.json ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "candidate": "merlin-v3-ungated-20261104",
3
+ "weights_sha256": "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1",
4
+ "threshold": 0.5,
5
+ "calibrator": {
6
+ "coefficient": 1.0,
7
+ "intercept": 0.0
8
+ },
9
+ "agreement": {
10
+ "sparse_model_sha256": "cddc99dd145eeb4e29ebfb35bc874408c2737b4f340917919024dc7e9113c229",
11
+ "encoder_calibrator": {
12
+ "coefficient": 0.4128351080107227,
13
+ "intercept": -1.3945806128701024
14
+ },
15
+ "sparse_calibrator": {
16
+ "coefficient": 1.2411713915407203,
17
+ "intercept": 0.12969558567752842
18
+ },
19
+ "operating_point": {
20
+ "threshold": 0.5,
21
+ "encoder_threshold": 0.5455402148631912,
22
+ "sparse_threshold": 0.2,
23
+ "rank": [
24
+ 0.900131845841785,
25
+ 0.9841269841269841,
26
+ 295,
27
+ 0.5455402148631912,
28
+ 0.2
29
+ ],
30
+ "validation": {
31
+ "selection": {
32
+ "local_staging": {
33
+ "n": 163,
34
+ "tp": 0,
35
+ "fp": 0,
36
+ "fn": 0,
37
+ "tn": 163,
38
+ "precision": null,
39
+ "recall": null,
40
+ "fpr": 0.0
41
+ },
42
+ "ASB-Traj": {
43
+ "n": 88,
44
+ "tp": 32,
45
+ "fp": 0,
46
+ "fn": 2,
47
+ "tn": 54,
48
+ "precision": 1.0,
49
+ "recall": 0.9411764705882353,
50
+ "fpr": 0.0
51
+ },
52
+ "AgentAlign-Traj": {
53
+ "n": 111,
54
+ "tp": 62,
55
+ "fp": 1,
56
+ "fn": 6,
57
+ "tn": 42,
58
+ "precision": 0.9841269841269841,
59
+ "recall": 0.9117647058823529,
60
+ "fpr": 0.023255813953488372
61
+ },
62
+ "authored": {
63
+ "n": 78,
64
+ "tp": 0,
65
+ "fp": 0,
66
+ "fn": 36,
67
+ "tn": 42,
68
+ "precision": null,
69
+ "recall": 0.0,
70
+ "fpr": 0.0
71
+ },
72
+ "benchmark": {
73
+ "n": 199,
74
+ "tp": 94,
75
+ "fp": 1,
76
+ "fn": 8,
77
+ "tn": 96,
78
+ "precision": 0.9894736842105263,
79
+ "recall": 0.9215686274509803,
80
+ "fpr": 0.010309278350515464
81
+ }
82
+ },
83
+ "calibration": {
84
+ "local_staging": {
85
+ "n": 132,
86
+ "tp": 0,
87
+ "fp": 0,
88
+ "fn": 0,
89
+ "tn": 132,
90
+ "precision": null,
91
+ "recall": null,
92
+ "fpr": 0.0
93
+ },
94
+ "ASB-Traj": {
95
+ "n": 56,
96
+ "tp": 23,
97
+ "fp": 0,
98
+ "fn": 2,
99
+ "tn": 31,
100
+ "precision": 1.0,
101
+ "recall": 0.92,
102
+ "fpr": 0.0
103
+ },
104
+ "AgentAlign-Traj": {
105
+ "n": 121,
106
+ "tp": 48,
107
+ "fp": 0,
108
+ "fn": 10,
109
+ "tn": 63,
110
+ "precision": 1.0,
111
+ "recall": 0.8275862068965517,
112
+ "fpr": 0.0
113
+ },
114
+ "authored": {
115
+ "n": 82,
116
+ "tp": 0,
117
+ "fp": 0,
118
+ "fn": 40,
119
+ "tn": 42,
120
+ "precision": null,
121
+ "recall": 0.0,
122
+ "fpr": 0.0
123
+ },
124
+ "benchmark": {
125
+ "n": 177,
126
+ "tp": 71,
127
+ "fp": 0,
128
+ "fn": 12,
129
+ "tn": 94,
130
+ "precision": 1.0,
131
+ "recall": 0.8554216867469879,
132
+ "fpr": 0.0
133
+ }
134
+ }
135
+ }
136
+ }
137
+ },
138
+ "default_enabled": true,
139
+ "score_semantics": "Threshold-relative agreement score; not a calibrated risk probability",
140
+ "weights_candidate": "merlin-v3-precision-20261007",
141
+ "runtime_gates": {
142
+ "operation": false,
143
+ "lexical_request": false,
144
+ "learned_request_intent": false,
145
+ "action_agreement": true
146
+ }
147
+ }
v3-ungated/native/stepsafety/model/native/manifest.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format": "kontext-merlin-native-safetensors-f32/v1",
3
+ "source_weights_sha256": "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1",
4
+ "tokenizer_sha256": "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031",
5
+ "quantized": false,
6
+ "shard_uncompressed_bytes": 67108864,
7
+ "files": [
8
+ {
9
+ "name": "agreement.json.gz",
10
+ "size_bytes": 2290715,
11
+ "sha256": "6ffbd35e56a031df9f2ca08f7520486cc8ef65592c8d03cfe966fbd42523bbdf"
12
+ },
13
+ {
14
+ "name": "request_gate.json.gz",
15
+ "size_bytes": 2492102,
16
+ "sha256": "acfb7025661b5a424d482aae1fd351f31f714bf9c22b7c18dcabb623ca2e72c3"
17
+ },
18
+ {
19
+ "name": "tokenizer.json.gz",
20
+ "size_bytes": 1736709,
21
+ "sha256": "956061a0f96d88a118848b4f32e4e5fcdc8c363f4cbc223fdc175232668aa41c"
22
+ },
23
+ {
24
+ "name": "weights.00.gz",
25
+ "size_bytes": 46014808,
26
+ "sha256": "2c69b3cccc79582da64dd47ab44835604b5b4f5c676b1102eacef17dc52cacad"
27
+ },
28
+ {
29
+ "name": "weights.01.gz",
30
+ "size_bytes": 41825526,
31
+ "sha256": "129de5fde020ece20ce7cf2529564c779e486e29b0c4725f7ed09af086f6db04"
32
+ },
33
+ {
34
+ "name": "weights.02.gz",
35
+ "size_bytes": 42596160,
36
+ "sha256": "7cb825c60a553c37d70ee6859d597a49b0afa0e34a2a243d62903da5d297f47e"
37
+ },
38
+ {
39
+ "name": "weights.03.gz",
40
+ "size_bytes": 62319867,
41
+ "sha256": "d5a9ce4740491f638c497b8e88d6d4236a1f26b3debfa9f7566a22c960828b6c"
42
+ },
43
+ {
44
+ "name": "weights.04.gz",
45
+ "size_bytes": 13716366,
46
+ "sha256": "9316ce90d589cba6e3ce02f5e643fa244ff7e7bb2b5117b8508f176f01ed3000"
47
+ }
48
+ ]
49
+ }
v3-ungated/native/stepsafety/model/native/request_gate.json.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:acfb7025661b5a424d482aae1fd351f31f714bf9c22b7c18dcabb623ca2e72c3
3
+ size 2492102
v3-ungated/native/stepsafety/model/native/tokenizer.json.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:956061a0f96d88a118848b4f32e4e5fcdc8c363f4cbc223fdc175232668aa41c
3
+ size 1736709
v3-ungated/native/stepsafety/model/native/weights.00.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2c69b3cccc79582da64dd47ab44835604b5b4f5c676b1102eacef17dc52cacad
3
+ size 46014808
v3-ungated/native/stepsafety/model/native/weights.01.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:129de5fde020ece20ce7cf2529564c779e486e29b0c4725f7ed09af086f6db04
3
+ size 41825526
v3-ungated/native/stepsafety/model/native/weights.02.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7cb825c60a553c37d70ee6859d597a49b0afa0e34a2a243d62903da5d297f47e
3
+ size 42596160
v3-ungated/native/stepsafety/model/native/weights.03.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5a9ce4740491f638c497b8e88d6d4236a1f26b3debfa9f7566a22c960828b6c
3
+ size 62319867
v3-ungated/native/stepsafety/model/native/weights.04.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9316ce90d589cba6e3ce02f5e643fa244ff7e7bb2b5117b8508f176f01ed3000
3
+ size 13716366
v3-ungated/native/stepsafety/modelstore.go ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ "path/filepath"
12
+ "sort"
13
+ "strings"
14
+ "time"
15
+ )
16
+
17
+ const legacyModelVersion = "toolsafe-deberta-v3-xsmall-onnx-scoped-v2"
18
+
19
+ type artifactSpec struct {
20
+ Name string `json:"name"`
21
+ Size int64 `json:"size_bytes"`
22
+ SHA256 string `json:"sha256"`
23
+ }
24
+
25
+ var requiredArtifacts = []artifactSpec{
26
+ {Name: "config.json", Size: 924, SHA256: "41f89ae28c4f0549b92537d0ef5918ef9e1af25e9898af6456e3551bf0a93852"},
27
+ {Name: "model.onnx", Size: 287233599, SHA256: "7f7c068c80e826cee69fd3fca089662572b3372b0be109c6cf17b6852d94557c"},
28
+ {Name: "tokenizer.json", Size: 8340654, SHA256: "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031"},
29
+ {Name: "tokenizer_config.json", Size: 601, SHA256: "141468772d3b095cc30275fd9c05dff92a78619c341487da9f5549a3564ea6cb"},
30
+ }
31
+
32
+ const (
33
+ sourceRevision = "9c63e6191598b0ba72947a4394ac8297c41053d1"
34
+ sourceArtifactPath = "artifacts/models/history_serialization/deberta_v3_xsmall"
35
+ sourceResultsPath = "results/history_serialization_ablation.json"
36
+ sourceResultsSHA256 = "09efd898c02dedb1ad8f83dd5b90888e84cbcf8919c51450c9289176c42db41a"
37
+ sourceFindingsPath = "docs/HISTORY_SERIALIZATION_ABLATION_FINDINGS.md"
38
+ sourceFindingsSHA256 = "f4231362a77e8d622ace51c4d8ac323fc801cca6ebc6771d5295b55df2bef08f"
39
+ sourceSerializerPath = "src/toolsafe_lab/history_serialization.py"
40
+ sourceSerializerSHA = "4b14a72a6825555d56c4be0549039428b78332940d3cdd25516ef278888642e2"
41
+ sourcePackingPath = "src/toolsafe_lab/standalone_encoder.py"
42
+ sourcePackingSHA = "ca8db9401025c8489a142d9447140a857fd77df84e5246fde92c995ccbe173ff"
43
+ sourceProtocolPath = "docs/HISTORY_SERIALIZATION_ABLATION_PROTOCOL.md"
44
+ sourceProtocolSHA = "6e44e1175be44573e6feef1130ef9f4faeaf8457025c79d5100212c568099089"
45
+ )
46
+
47
+ const microsoftMITLicense = `MIT License
48
+
49
+ Copyright (c) Microsoft Corporation.
50
+
51
+ Permission is hereby granted, free of charge, to any person obtaining a copy
52
+ of this software and associated documentation files (the "Software"), to deal
53
+ in the Software without restriction, including without limitation the rights
54
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
55
+ copies of the Software, and to permit persons to whom the Software is
56
+ furnished to do so, subject to the following conditions:
57
+
58
+ The above copyright notice and this permission notice shall be included in all
59
+ copies or substantial portions of the Software.
60
+
61
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
62
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
63
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
64
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
65
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
66
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
67
+ SOFTWARE
68
+ `
69
+
70
+ type Provenance struct {
71
+ RuntimeFormat string `json:"runtime_format"`
72
+ InputPolicy string `json:"input_policy"`
73
+ SourceWeightsSHA256 string `json:"source_weights_sha256"`
74
+ ExportToolchain string `json:"export_toolchain"`
75
+ EvaluationScope string `json:"evaluation_scope"`
76
+
77
+ SchemaVersion int `json:"schema_version"`
78
+ ModelVersion string `json:"model_version"`
79
+ SourceProject string `json:"source_project"`
80
+ SourceRevision string `json:"source_revision"`
81
+ SourceArtifactPath string `json:"source_artifact_path,omitempty"`
82
+ SourceResultsPath string `json:"source_results_path,omitempty"`
83
+ SourceResultsSHA256 string `json:"source_results_sha256,omitempty"`
84
+ SourceFindingsPath string `json:"source_findings_path,omitempty"`
85
+ SourceFindingsSHA256 string `json:"source_findings_sha256,omitempty"`
86
+ SourceSerializerPath string `json:"source_serializer_path,omitempty"`
87
+ SourceSerializerSHA string `json:"source_serializer_sha256,omitempty"`
88
+ SourcePackingPath string `json:"source_packing_path,omitempty"`
89
+ SourcePackingSHA string `json:"source_packing_sha256,omitempty"`
90
+ SourceProtocolPath string `json:"source_protocol_path,omitempty"`
91
+ SourceProtocolSHA string `json:"source_protocol_sha256,omitempty"`
92
+ BaseModel string `json:"base_model"`
93
+ BaseModelRevision string `json:"base_model_revision"`
94
+ BaseModelLicense string `json:"base_model_license"`
95
+ InputMode string `json:"input_mode"`
96
+ ThoughtIncluded bool `json:"thought_included"`
97
+ HistorySerialization historySerializationProvenance `json:"history_serialization"`
98
+ Evaluation evaluationProvenance `json:"evaluation"`
99
+ MaxLength int `json:"max_length"`
100
+ FieldMarkers map[string]string `json:"field_markers"`
101
+ FieldBudgets map[string]int `json:"field_budgets"`
102
+ CalibrationScale float64 `json:"calibration_scale"`
103
+ CalibrationBias float64 `json:"calibration_bias"`
104
+ InitialThreshold float64 `json:"initial_threshold"`
105
+ ImportedAt time.Time `json:"imported_at"`
106
+ Artifacts []artifactSpec `json:"artifacts"`
107
+ }
108
+
109
+ type historySerializationProvenance struct {
110
+ EventFields []string `json:"event_fields"`
111
+ EmptyHistory string `json:"empty_history"`
112
+ Observations string `json:"observations"`
113
+ JSONEncoding string `json:"json_encoding"`
114
+ }
115
+
116
+ type evaluationProvenance struct {
117
+ Threshold float64 `json:"threshold"`
118
+ Accuracy float64 `json:"accuracy"`
119
+ Precision float64 `json:"precision"`
120
+ Recall float64 `json:"recall"`
121
+ F1 float64 `json:"f1"`
122
+ MCC float64 `json:"mcc"`
123
+ WorstSourceRecall float64 `json:"worst_source_recall"`
124
+ }
125
+
126
+ func DefaultModelDir(dbPath string) string {
127
+ root := filepath.Join(filepath.Dir(dbPath), "judge-models")
128
+ return filepath.Join(root, "toolsafe", legacyModelVersion)
129
+ }
130
+
131
+ func ValidateModelDir(dir string) error {
132
+ if strings.TrimSpace(dir) == "" {
133
+ return backendError(ErrorUnavailable, errors.New("step-safety model directory is empty"))
134
+ }
135
+ for _, artifact := range requiredArtifacts {
136
+ path := filepath.Join(dir, artifact.Name)
137
+ info, err := os.Stat(path)
138
+ if err != nil {
139
+ return backendError(ErrorUnavailable, fmt.Errorf("model artifact %s is unavailable", artifact.Name))
140
+ }
141
+ if !info.Mode().IsRegular() || info.Size() != artifact.Size {
142
+ return backendError(ErrorUnavailable, fmt.Errorf("model artifact %s has unexpected size", artifact.Name))
143
+ }
144
+ digest, err := fileSHA256(path)
145
+ if err != nil || digest != artifact.SHA256 {
146
+ return backendError(ErrorUnavailable, fmt.Errorf("model artifact %s failed checksum verification", artifact.Name))
147
+ }
148
+ }
149
+ return nil
150
+ }
151
+
152
+ // InstallModel copies only inference artifacts into Kontext's existing
153
+ // database-adjacent model cache. It never imports the 849 MB training resume
154
+ // checkpoint and leaves no reference to the source tree.
155
+ func InstallModel(sourceDir, destinationDir string) (string, error) {
156
+ if err := ValidateModelDir(sourceDir); err != nil {
157
+ return "", err
158
+ }
159
+ if err := ValidateModelDir(destinationDir); err == nil {
160
+ return destinationDir, nil
161
+ } else if _, statErr := os.Stat(destinationDir); statErr == nil {
162
+ return "", fmt.Errorf("step-safety destination already exists but is invalid: %s", destinationDir)
163
+ } else if !errors.Is(statErr, os.ErrNotExist) {
164
+ return "", statErr
165
+ }
166
+ parent := filepath.Dir(destinationDir)
167
+ if err := os.MkdirAll(parent, 0o700); err != nil {
168
+ return "", err
169
+ }
170
+ temporary, err := os.MkdirTemp(parent, ".step-safety-install-*")
171
+ if err != nil {
172
+ return "", err
173
+ }
174
+ defer os.RemoveAll(temporary)
175
+
176
+ for _, artifact := range requiredArtifacts {
177
+ if err := copyArtifact(filepath.Join(sourceDir, artifact.Name), filepath.Join(temporary, artifact.Name)); err != nil {
178
+ return "", err
179
+ }
180
+ }
181
+ if err := os.WriteFile(filepath.Join(temporary, "LICENSE"), []byte(microsoftMITLicense), 0o600); err != nil {
182
+ return "", err
183
+ }
184
+ provenance := Provenance{
185
+ RuntimeFormat: "ONNX opset 17, float32, [1,512] int64 input_ids/attention_mask, [1,2] logits",
186
+ InputPolicy: "scoped-v2: exclude file tools; whole request/action/schema or abstain; filtered history uses first 72 and last 72 tokens, matching training",
187
+ SourceWeightsSHA256: "075d68f8bb4c1b4b98e2d2b2c0d5b013056b202172b080b34e7364b698844c86",
188
+ ExportToolchain: "torch 2.13.0; transformers 5.14.1; onnx 1.22.0; legacy exporter; scripts/step-safety/export_onnx.py",
189
+ EvaluationScope: "Original held-out TS-Bench checkpoint evaluation; not an estimate for scoped-v2 production coverage",
190
+
191
+ SchemaVersion: 3,
192
+ ModelVersion: legacyModelVersion,
193
+ SourceProject: "kontext-security/toolsafe-lab",
194
+ SourceRevision: sourceRevision,
195
+ SourceArtifactPath: sourceArtifactPath,
196
+ SourceResultsPath: sourceResultsPath,
197
+ SourceResultsSHA256: sourceResultsSHA256,
198
+ SourceFindingsPath: sourceFindingsPath,
199
+ SourceFindingsSHA256: sourceFindingsSHA256,
200
+ SourceSerializerPath: sourceSerializerPath,
201
+ SourceSerializerSHA: sourceSerializerSHA,
202
+ SourcePackingPath: sourcePackingPath,
203
+ SourcePackingSHA: sourcePackingSHA,
204
+ SourceProtocolPath: sourceProtocolPath,
205
+ SourceProtocolSHA: sourceProtocolSHA,
206
+ BaseModel: "microsoft/deberta-v3-xsmall",
207
+ BaseModelRevision: "4b419818330868dff6a60ad3e6b1c730f8b8c0c6",
208
+ BaseModelLicense: "MIT",
209
+ InputMode: "execution_context + canonical structured history (agent Thought excluded)",
210
+ ThoughtIncluded: false,
211
+ HistorySerialization: historySerializationProvenance{
212
+ EventFields: []string{"tool", "arguments", "observation"},
213
+ EmptyHistory: "[]",
214
+ Observations: "strings",
215
+ JSONEncoding: "UTF-8, sorted keys, compact separators",
216
+ },
217
+ Evaluation: evaluationProvenance{
218
+ Threshold: 0.5,
219
+ Accuracy: 0.9118629908103593,
220
+ Precision: 0.9266195885784464,
221
+ Recall: 0.8845252051582649,
222
+ F1: 0.9050832208726945,
223
+ MCC: 0.8236797425872459,
224
+ WorstSourceRecall: 0.5085227272727273,
225
+ },
226
+ MaxLength: 512,
227
+ FieldMarkers: map[string]string{
228
+ "request": "[USER_REQUEST]",
229
+ "history": "[INTERACTION_HISTORY]",
230
+ "action": "[CURRENT_ACTION]",
231
+ "schema": "[TOOL_DESCRIPTIONS]",
232
+ },
233
+ FieldBudgets: map[string]int{
234
+ "request": 96,
235
+ "history": 144,
236
+ "action": 128,
237
+ "schema": 128,
238
+ },
239
+ CalibrationScale: 1.427213430140093,
240
+ CalibrationBias: 2.953687013257505,
241
+ InitialThreshold: 0.5,
242
+ ImportedAt: time.Now().UTC(),
243
+ Artifacts: append([]artifactSpec(nil), requiredArtifacts...),
244
+ }
245
+ encoded, err := json.MarshalIndent(provenance, "", " ")
246
+ if err != nil {
247
+ return "", err
248
+ }
249
+ if err := os.WriteFile(filepath.Join(temporary, "PROVENANCE.json"), append(encoded, '\n'), 0o600); err != nil {
250
+ return "", err
251
+ }
252
+ if err := ValidateModelDir(temporary); err != nil {
253
+ return "", err
254
+ }
255
+ if err := os.Rename(temporary, destinationDir); err != nil {
256
+ return "", err
257
+ }
258
+ return destinationDir, nil
259
+ }
260
+
261
+ func RequiredArtifacts() []string {
262
+ names := make([]string, 0, len(requiredArtifacts))
263
+ for _, artifact := range requiredArtifacts {
264
+ names = append(names, artifact.Name)
265
+ }
266
+ sort.Strings(names)
267
+ return names
268
+ }
269
+
270
+ func copyArtifact(source, destination string) error {
271
+ in, err := os.Open(source)
272
+ if err != nil {
273
+ return err
274
+ }
275
+ defer in.Close()
276
+ out, err := os.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
277
+ if err != nil {
278
+ return err
279
+ }
280
+ _, copyErr := io.Copy(out, in)
281
+ closeErr := out.Close()
282
+ return errors.Join(copyErr, closeErr)
283
+ }
284
+
285
+ func fileSHA256(path string) (string, error) {
286
+ file, err := os.Open(path)
287
+ if err != nil {
288
+ return "", err
289
+ }
290
+ defer file.Close()
291
+ digest := sha256.New()
292
+ if _, err := io.Copy(digest, file); err != nil {
293
+ return "", err
294
+ }
295
+ return hex.EncodeToString(digest.Sum(nil)), nil
296
+ }
v3-ungated/native/stepsafety/native_assets.go ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:build !nostepsafetymodel
2
+
3
+ package stepsafety
4
+
5
+ import "embed"
6
+
7
+ // nativeAssets holds the checkpoint, tokenizer and classifiers. They are about
8
+ // 213 MB of the binary; the nostepsafetymodel tag leaves them out.
9
+ //
10
+ //go:embed model/native/*.gz
11
+ var nativeAssets embed.FS
12
+
13
+ // modelEmbedded reports whether this build carries the model; tests that
14
+ // need it skip without it.
15
+ const modelEmbedded = true
v3-ungated/native/stepsafety/native_assets_nomodel.go ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:build nostepsafetymodel
2
+
3
+ package stepsafety
4
+
5
+ import "embed"
6
+
7
+ // nativeAssets is empty in a build without the model, for hosts that cannot
8
+ // carry the full binary. Loading then fails, so New returns an unavailable
9
+ // evaluator and health reports "unavailable"; decisions do not depend on it.
10
+ var nativeAssets embed.FS
11
+
12
+ // modelEmbedded reports whether this build carries the model; tests that
13
+ // need it skip without it.
14
+ const modelEmbedded = false
v3-ungated/native/stepsafety/native_backend.go ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "compress/gzip"
5
+ "context"
6
+ "crypto/sha256"
7
+ "encoding/hex"
8
+ "errors"
9
+ "io"
10
+ "io/fs"
11
+ "sync"
12
+ "sync/atomic"
13
+ )
14
+
15
+ var nativeOnce sync.Once
16
+ var nativeReady = make(chan struct{})
17
+ var nativeShared struct {
18
+ model *nativeModel
19
+ tokenizer *tokenizer
20
+ err error
21
+ }
22
+
23
+ type nativeBackend struct {
24
+ model *nativeModel
25
+ tokenizer *tokenizer
26
+ modelVersion string
27
+ closed atomic.Bool
28
+ }
29
+
30
+ // Load one immutable, checksum-verified checkpoint per process, just as Kestrel
31
+ // does. No files are extracted, no library is loaded, and no download occurs.
32
+ // A caller's startup deadline never poisons the shared load for later callers.
33
+ func newNativeBackend(ctx context.Context, cfg Config) (*nativeBackend, error) {
34
+ ctx, cancel := context.WithTimeout(ctx, cfg.StartupTimeout)
35
+ defer cancel()
36
+ nativeOnce.Do(func() {
37
+ go func() {
38
+ defer close(nativeReady)
39
+ nativeShared.model, nativeShared.tokenizer, nativeShared.err = loadNativeAssets(context.Background())
40
+ if nativeShared.err == nil {
41
+ nativeShared.err = loadAgreementAssets()
42
+ if nativeShared.err == nil {
43
+ nativeShared.err = loadRequestGateAssets()
44
+ }
45
+ }
46
+ }()
47
+ })
48
+ select {
49
+ case <-ctx.Done():
50
+ return nil, ctx.Err()
51
+ case <-nativeReady:
52
+ if nativeShared.err != nil {
53
+ return nil, backendError(ErrorUnavailable, nativeShared.err)
54
+ }
55
+ return &nativeBackend{model: nativeShared.model, tokenizer: nativeShared.tokenizer, modelVersion: cfg.ModelVersion}, nil
56
+ }
57
+ }
58
+
59
+ func loadNativeAssets(ctx context.Context) (*nativeModel, *tokenizer, error) {
60
+ names, err := fs.Glob(nativeAssets, "model/native/weights.*.gz")
61
+ if err != nil || len(names) != 5 {
62
+ return nil, nil, errors.New("missing embedded native weights")
63
+ }
64
+ readers := make([]io.Reader, 0, len(names))
65
+ for _, name := range names {
66
+ f, err := nativeAssets.Open(name)
67
+ if err != nil {
68
+ return nil, nil, err
69
+ }
70
+ defer f.Close()
71
+ readers = append(readers, f)
72
+ }
73
+ r, err := gzip.NewReader(io.MultiReader(readers...))
74
+ if err != nil {
75
+ return nil, nil, err
76
+ }
77
+ defer r.Close()
78
+ weights, err := readNativeWeights(ctx, r)
79
+ if err != nil {
80
+ return nil, nil, err
81
+ }
82
+ model, err := buildNativeModel(ctx, weights)
83
+ if err != nil {
84
+ return nil, nil, err
85
+ }
86
+ f, err := nativeAssets.Open("model/native/tokenizer.json.gz")
87
+ if err != nil {
88
+ return nil, nil, err
89
+ }
90
+ defer f.Close()
91
+ t, err := gzip.NewReader(f)
92
+ if err != nil {
93
+ return nil, nil, err
94
+ }
95
+ defer t.Close()
96
+ data, err := io.ReadAll(io.LimitReader(t, 8340655))
97
+ if err != nil {
98
+ return nil, nil, err
99
+ }
100
+ digest := sha256.Sum256(data)
101
+ if len(data) != 8340654 || hex.EncodeToString(digest[:]) != "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031" {
102
+ return nil, nil, errors.New("embedded tokenizer checksum mismatch")
103
+ }
104
+ tok, err := parseTokenizer(data)
105
+ return model, tok, err
106
+ }
107
+
108
+ func (b *nativeBackend) Infer(ctx context.Context, input Input) (InferenceResult, error) {
109
+ if b.closed.Load() {
110
+ return InferenceResult{}, backendError(ErrorUnavailable, nil)
111
+ }
112
+ packed, err := packScopedInput(ctx, b.tokenizer, input)
113
+ if err != nil {
114
+ return InferenceResult{}, err
115
+ }
116
+ out, err := b.model.infer(ctx, packed)
117
+ if err != nil {
118
+ return out, err
119
+ }
120
+ return applyAgreement(ctx, input, out)
121
+ }
122
+
123
+ func (b *nativeBackend) Health(ctx context.Context) (Health, error) {
124
+ if err := ctx.Err(); err != nil {
125
+ return Health{}, err
126
+ }
127
+ if b.closed.Load() {
128
+ return Health{}, backendError(ErrorUnavailable, nil)
129
+ }
130
+ return Health{Status: "ready", ModelVersion: b.modelVersion, Device: "go-cpu"}, nil
131
+ }
132
+
133
+ func (b *nativeBackend) Close() error { b.closed.Store(true); return nil }
v3-ungated/native/stepsafety/native_linear_arm64.go ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:build arm64 && !purego
2
+
3
+ package stepsafety
4
+
5
+ import (
6
+ "runtime"
7
+ "sync"
8
+
9
+ "gonum.org/v1/gonum/blas"
10
+ "gonum.org/v1/gonum/blas/gonum"
11
+ )
12
+
13
+ // Pack each group of eight output columns together. The NEON kernel consumes
14
+ // four rows and eight columns without a C ABI, dynamic library, or cgo.
15
+ func prepareNativeLinear(l nativeLinear) nativeLinear {
16
+ return prepareNativeLinearInto(l, nil)
17
+ }
18
+
19
+ func prepareNativeLinearInto(l nativeLinear, scratch []float32) nativeLinear {
20
+ if l.out < 8 || len(l.weight) != l.in*l.out {
21
+ return l
22
+ }
23
+ l.packed = nativeBuffer(scratch, (l.out+7)/8*8*l.in)
24
+ for j := 0; j < l.out; j += 8 {
25
+ for k := 0; k < l.in; k++ {
26
+ for c := 0; c < 8; c++ {
27
+ if j+c < l.out {
28
+ l.packed[j*l.in+k*8+c] = l.weight[(j+c)*l.in+k]
29
+ }
30
+ }
31
+ }
32
+ }
33
+ l.weight = nil
34
+ return l
35
+ }
36
+
37
+ func nativeLinearProduct(l nativeLinear, x []float32, rows int) []float32 {
38
+ return nativeLinearProductInto(l, x, rows, nil)
39
+ }
40
+
41
+ func nativeLinearProductInto(l nativeLinear, x []float32, rows int, scratch []float32) []float32 {
42
+ if len(l.packed) == 0 {
43
+ out := nativeBuffer(scratch, rows*l.out)
44
+ gonum.Implementation{}.Sgemm(blas.NoTrans, blas.Trans, rows, l.out, l.in, 1, x, l.in, l.weight, l.in, 0, out, l.out)
45
+ return out
46
+ }
47
+ padded := (rows + 3) / 4 * 4
48
+ if rows != padded {
49
+ if cap(x) >= padded*l.in {
50
+ x = x[:padded*l.in]
51
+ } else {
52
+ temp := make([]float32, padded*l.in)
53
+ copy(temp, x)
54
+ x = temp
55
+ }
56
+ }
57
+ stride := (l.out + 7) / 8 * 8
58
+ out := nativeBuffer(scratch, padded*stride)
59
+ workers := min(4, runtime.GOMAXPROCS(0), padded/4)
60
+ var wg sync.WaitGroup
61
+ for worker := 0; worker < workers; worker++ {
62
+ wg.Add(1)
63
+ go func(worker int) {
64
+ defer wg.Done()
65
+ for i := worker * 4; i < padded; i += workers * 4 {
66
+ for j := 0; j < l.out; j += 8 {
67
+ nativeMatmul4x8(&x[i*l.in], &l.packed[j*l.in], &out[i*stride+j], l.in, stride)
68
+ }
69
+ }
70
+ }(worker)
71
+ }
72
+ wg.Wait()
73
+ if stride != l.out {
74
+ for i := 0; i < rows; i++ {
75
+ copy(out[i*l.out:(i+1)*l.out], out[i*stride:i*stride+l.out])
76
+ }
77
+ }
78
+ return out[:rows*l.out]
79
+ }
80
+
81
+ //go:noescape
82
+ func nativeMatmul4x8(a, packedB, c *float32, k, strideC int)
v3-ungated/native/stepsafety/native_linear_arm64.s ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:build arm64 && !purego
2
+
3
+ #include "textflag.h"
4
+
5
+ // C[4,8] = A[4,k] * B[k,8]. All dimensions checked by the Go caller.
6
+ // B is a contiguous packed panel; C rows have strideC elements.
7
+ TEXT ·nativeMatmul4x8(SB), NOSPLIT, $0-40
8
+ MOVD a+0(FP), R0
9
+ MOVD packedB+8(FP), R1
10
+ MOVD c+16(FP), R2
11
+ MOVD k+24(FP), R3
12
+ MOVD strideC+32(FP), R4
13
+ LSL $2, R3, R5
14
+ LSL $2, R4, R4
15
+ ADD R5, R0, R6
16
+ ADD R5, R6, R7
17
+ ADD R5, R7, R8
18
+ VEOR V16.B16, V16.B16, V16.B16
19
+ VEOR V17.B16, V17.B16, V17.B16
20
+ VEOR V18.B16, V18.B16, V18.B16
21
+ VEOR V19.B16, V19.B16, V19.B16
22
+ VEOR V20.B16, V20.B16, V20.B16
23
+ VEOR V21.B16, V21.B16, V21.B16
24
+ VEOR V22.B16, V22.B16, V22.B16
25
+ VEOR V23.B16, V23.B16, V23.B16
26
+ loop:
27
+ FMOVS (R0), F0
28
+ FMOVS (R6), F1
29
+ FMOVS (R7), F2
30
+ FMOVS (R8), F3
31
+ VDUP V0.S[0], V0.S4
32
+ VDUP V1.S[0], V1.S4
33
+ VDUP V2.S[0], V2.S4
34
+ VDUP V3.S[0], V3.S4
35
+ VLD1.P 32(R1), [V4.S4, V5.S4]
36
+ VFMLA V4.S4, V0.S4, V16.S4
37
+ VFMLA V5.S4, V0.S4, V17.S4
38
+ VFMLA V4.S4, V1.S4, V18.S4
39
+ VFMLA V5.S4, V1.S4, V19.S4
40
+ VFMLA V4.S4, V2.S4, V20.S4
41
+ VFMLA V5.S4, V2.S4, V21.S4
42
+ VFMLA V4.S4, V3.S4, V22.S4
43
+ VFMLA V5.S4, V3.S4, V23.S4
44
+ ADD $4, R0, R0
45
+ ADD $4, R6, R6
46
+ ADD $4, R7, R7
47
+ ADD $4, R8, R8
48
+ SUBS $1, R3, R3
49
+ BNE loop
50
+ VST1 [V16.S4, V17.S4], (R2)
51
+ ADD R4, R2, R2
52
+ VST1 [V18.S4, V19.S4], (R2)
53
+ ADD R4, R2, R2
54
+ VST1 [V20.S4, V21.S4], (R2)
55
+ ADD R4, R2, R2
56
+ VST1 [V22.S4, V23.S4], (R2)
57
+ RET
v3-ungated/native/stepsafety/native_linear_generic.go ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:build !arm64 || purego
2
+
3
+ package stepsafety
4
+
5
+ import (
6
+ "gonum.org/v1/gonum/blas"
7
+ "gonum.org/v1/gonum/blas/gonum"
8
+ )
9
+
10
+ func prepareNativeLinear(l nativeLinear) nativeLinear { return l }
11
+ func prepareNativeLinearInto(l nativeLinear, _ []float32) nativeLinear { return l }
12
+
13
+ func nativeLinearProduct(l nativeLinear, x []float32, rows int) []float32 {
14
+ return nativeLinearProductInto(l, x, rows, nil)
15
+ }
16
+ func nativeLinearProductInto(l nativeLinear, x []float32, rows int, scratch []float32) []float32 {
17
+ out := nativeBuffer(scratch, rows*l.out)
18
+ gonum.Implementation{}.Sgemm(blas.NoTrans, blas.Trans, rows, l.out, l.in, 1, x, l.in, l.weight, l.in, 0, out, l.out)
19
+ return out
20
+ }
v3-ungated/native/stepsafety/native_model.go ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "math"
8
+ "sync"
9
+ )
10
+
11
+ const (
12
+ nativeHidden = 384
13
+ nativeHeads = 6
14
+ nativeHeadSize = 64
15
+ nativeIntermediate = 1536
16
+ nativeLayers = 12
17
+ nativePositions = 512
18
+ )
19
+
20
+ // This is the pinned DeBERTa-v3-xsmall evaluation graph, not a general-purpose
21
+ // transformer interpreter. Dropout is disabled; there are no token-type,
22
+ // absolute-position, convolution, or decoder layers in this checkpoint.
23
+ type nativeModel struct {
24
+ embeddings []float32
25
+ embeddingNorm nativeNorm
26
+ layers [nativeLayers]nativeLayer
27
+ pooler, classifier nativeLinear
28
+ work sync.Pool
29
+ }
30
+ type nativeNorm struct{ weight, bias []float32 }
31
+ type nativeLinear struct {
32
+ weight, bias []float32
33
+ packed []float32
34
+ in, out int
35
+ }
36
+ type nativeLayer struct {
37
+ query, key, value, attention, intermediate, output nativeLinear
38
+ attentionNorm, outputNorm nativeNorm
39
+ relative [nativeHeads]struct{ query, key nativeLinear }
40
+ }
41
+
42
+ func buildNativeModel(ctx context.Context, tensors map[string]nativeTensor) (*nativeModel, error) {
43
+ m := &nativeModel{}
44
+ var firstErr error
45
+ weight := func(name string, shape ...int) []float32 {
46
+ data, err := nativeWeight(tensors, name, shape...)
47
+ if firstErr == nil {
48
+ firstErr = err
49
+ }
50
+ return data
51
+ }
52
+ norm := func(name string) nativeNorm {
53
+ return nativeNorm{weight(name+".weight", nativeHidden), weight(name+".bias", nativeHidden)}
54
+ }
55
+ linear := func(name string, in, out int) nativeLinear {
56
+ return prepareNativeLinear(nativeLinear{weight: weight(name+".weight", out, in), bias: weight(name+".bias", out), in: in, out: out})
57
+ }
58
+ m.embeddings = weight("deberta.embeddings.word_embeddings.weight", 128005, nativeHidden)
59
+ m.embeddingNorm = norm("deberta.embeddings.LayerNorm")
60
+ rel := weight("deberta.encoder.rel_embeddings.weight", nativePositions, nativeHidden)
61
+ relNorm := norm("deberta.encoder.LayerNorm")
62
+ for i := range m.layers {
63
+ p := fmt.Sprintf("deberta.encoder.layer.%d.", i)
64
+ l := &m.layers[i]
65
+ l.query = linear(p+"attention.self.query_proj", nativeHidden, nativeHidden)
66
+ l.key = linear(p+"attention.self.key_proj", nativeHidden, nativeHidden)
67
+ l.value = linear(p+"attention.self.value_proj", nativeHidden, nativeHidden)
68
+ l.attention = linear(p+"attention.output.dense", nativeHidden, nativeHidden)
69
+ l.attentionNorm = norm(p + "attention.output.LayerNorm")
70
+ l.intermediate = linear(p+"intermediate.dense", nativeHidden, nativeIntermediate)
71
+ l.output = linear(p+"output.dense", nativeIntermediate, nativeHidden)
72
+ l.outputNorm = norm(p + "output.LayerNorm")
73
+ }
74
+ m.pooler = linear("pooler.dense", nativeHidden, nativeHidden)
75
+ m.classifier = linear("classifier", nativeHidden, 2)
76
+ if firstErr != nil {
77
+ return nil, firstErr
78
+ }
79
+ // Relative embeddings and their projections depend only on weights. Cache
80
+ // them once; every inference uses the same exact float32 parameters.
81
+ rel = append([]float32(nil), rel...)
82
+ relNorm.apply(rel, nil)
83
+ for i := range m.layers {
84
+ if err := ctx.Err(); err != nil {
85
+ return nil, err
86
+ }
87
+ l := &m.layers[i]
88
+ relQ, relK := l.query.apply(rel, nativePositions), l.key.apply(rel, nativePositions)
89
+ for h := 0; h < nativeHeads; h++ {
90
+ l.relative[h].query = prepareNativeLinear(nativeLinear{weight: nativeHead(relQ, nativePositions, h), in: nativeHeadSize, out: nativePositions})
91
+ l.relative[h].key = prepareNativeLinear(nativeLinear{weight: nativeHead(relK, nativePositions, h), in: nativeHeadSize, out: nativePositions})
92
+ }
93
+ }
94
+ return m, nil
95
+ }
96
+
97
+ func (l nativeLinear) apply(x []float32, rows int) []float32 {
98
+ return l.applyInto(x, rows, nil)
99
+ }
100
+ func (l nativeLinear) applyInto(x []float32, rows int, scratch []float32) []float32 {
101
+ out := nativeLinearProductInto(l, x, rows, scratch)
102
+ for i := 0; i < rows; i++ {
103
+ row := out[i*l.out : (i+1)*l.out]
104
+ for j := range row {
105
+ row[j] += l.bias[j]
106
+ }
107
+ }
108
+ return out
109
+ }
110
+
111
+ func (norm nativeNorm) apply(x, residual []float32) {
112
+ for start := 0; start < len(x); start += nativeHidden {
113
+ row := x[start : start+nativeHidden]
114
+ var mean float64
115
+ for j := range row {
116
+ if residual != nil {
117
+ row[j] += residual[start+j]
118
+ }
119
+ mean += float64(row[j])
120
+ }
121
+ mean /= nativeHidden
122
+ var variance float64
123
+ for _, v := range row {
124
+ d := float64(v) - mean
125
+ variance += d * d
126
+ }
127
+ inv := float32(1 / math.Sqrt(variance/nativeHidden+1e-7))
128
+ mu := float32(mean)
129
+ for j := range row {
130
+ row[j] = (row[j]-mu)*inv*norm.weight[j] + norm.bias[j]
131
+ }
132
+ }
133
+ }
134
+
135
+ func nativeGELU(x []float32) {
136
+ for i, v := range x {
137
+ x[i] = v * 0.5 * (1 + float32(math.Erf(float64(v)/math.Sqrt2)))
138
+ }
139
+ }
140
+
141
+ // Trimming trailing masked padding is exact for this encoder: valid queries
142
+ // never attend to padding, and all other operations are per-token. Preserve
143
+ // every real token, its original position, and the checkpoint's relative buckets.
144
+ func nativeSequenceLength(packed packedInput) (int, error) {
145
+ if len(packed.IDs) != maxSequenceLength || len(packed.Mask) != maxSequenceLength {
146
+ return 0, errors.New("invalid native input shape")
147
+ }
148
+ n := 0
149
+ for i, mask := range packed.Mask {
150
+ if packed.IDs[i] < 0 || packed.IDs[i] >= 128005 {
151
+ return 0, errors.New("invalid native token")
152
+ }
153
+ if mask == 1 && i == n {
154
+ n++
155
+ } else if mask != 0 {
156
+ return 0, errors.New("non-contiguous native attention mask")
157
+ }
158
+ }
159
+ if n == 0 {
160
+ return 0, errors.New("empty native input")
161
+ }
162
+ return n, nil
163
+ }
164
+
165
+ func nativeRelativePosition(distance int) int {
166
+ abs := distance
167
+ if abs < 0 {
168
+ abs = -abs
169
+ }
170
+ if abs <= 128 {
171
+ return distance
172
+ }
173
+ // Mirror make_log_bucket_position(bucket_size=256,max_position=512).
174
+ bucket := int(math.Ceil(math.Log(float64(abs)/128)/math.Log(511.0/128)*127)) + 128
175
+ if distance < 0 {
176
+ return -bucket
177
+ }
178
+ return bucket
179
+ }
180
+
181
+ func (m *nativeModel) infer(ctx context.Context, packed packedInput) (InferenceResult, error) {
182
+ n, err := nativeSequenceLength(packed)
183
+ if err != nil {
184
+ return InferenceResult{}, err
185
+ }
186
+ w, ok := m.work.Get().(*nativeWorkspace)
187
+ if !ok {
188
+ w = newNativeWorkspace()
189
+ }
190
+ defer m.work.Put(w)
191
+ x := w.x[:n*nativeHidden]
192
+ for i, id := range packed.IDs[:n] {
193
+ copy(x[i*nativeHidden:], m.embeddings[int(id)*nativeHidden:(int(id)+1)*nativeHidden])
194
+ }
195
+ m.embeddingNorm.apply(x, nil)
196
+ positions := w.positions[:n*n]
197
+ for i := 0; i < n; i++ {
198
+ for j := 0; j < n; j++ {
199
+ positions[i*n+j] = min(max(nativeRelativePosition(i-j)+256, 0), 511)
200
+ }
201
+ }
202
+ for _, l := range m.layers {
203
+ if err := ctx.Err(); err != nil {
204
+ return InferenceResult{}, err
205
+ }
206
+ q, k, v := l.query.applyInto(x, n, w.q), l.key.applyInto(x, n, w.k), l.value.applyInto(x, n, w.v)
207
+ attention := w.attention[:n*nativeHidden]
208
+ var wg sync.WaitGroup
209
+ for head := 0; head < nativeHeads; head++ {
210
+ wg.Add(1)
211
+ go func(head int) {
212
+ defer wg.Done()
213
+ nativeAttention(ctx, q, k, v, l.relative[head].query, l.relative[head].key, positions, attention, n, head, &w.heads[head])
214
+ }(head)
215
+ }
216
+ wg.Wait()
217
+ if err := ctx.Err(); err != nil {
218
+ return InferenceResult{}, err
219
+ }
220
+ a := l.attention.applyInto(attention, n, w.a)
221
+ l.attentionNorm.apply(a, x)
222
+ intermediate := l.intermediate.applyInto(a, n, w.intermediate)
223
+ nativeGELU(intermediate)
224
+ x = l.output.applyInto(intermediate, n, w.x)
225
+ l.outputNorm.apply(x, a)
226
+ }
227
+ if err := ctx.Err(); err != nil {
228
+ return InferenceResult{}, err
229
+ }
230
+ pooled := m.pooler.apply(x[:nativeHidden], 1)
231
+ nativeGELU(pooled)
232
+ logits := m.classifier.apply(pooled, 1)
233
+ return InferenceResult{Logits: [2]float64{float64(logits[0]), float64(logits[1])}, HistoryOmitted: packed.HistoryOmitted}, nil
234
+ }
235
+
236
+ func nativeHead(x []float32, n, head int) []float32 {
237
+ return nativeHeadInto(x, n, head, nil)
238
+ }
239
+ func nativeHeadInto(x []float32, n, head int, scratch []float32) []float32 {
240
+ out := nativeBuffer(scratch, n*nativeHeadSize)
241
+ for i := 0; i < n; i++ {
242
+ copy(out[i*nativeHeadSize:], x[i*nativeHidden+head*nativeHeadSize:i*nativeHidden+(head+1)*nativeHeadSize])
243
+ }
244
+ return out
245
+ }
246
+
247
+ func nativeAttention(ctx context.Context, q, k, v []float32, relQ, relK nativeLinear, positions []int, output []float32, n, head int, w *nativeHeadWorkspace) {
248
+ scale := float32(math.Sqrt(nativeHeadSize * 3))
249
+ qh, kh, vh := nativeHeadInto(q, n, head, w.q), nativeHeadInto(k, n, head, w.k), nativeHeadInto(v, n, head, w.v)
250
+ scores := nativeLinearProductInto(prepareNativeLinearInto(nativeLinear{weight: kh, in: nativeHeadSize, out: n}, w.packedK), qh, n, w.scores)
251
+ c2p := nativeLinearProductInto(relK, qh, n, w.c2p)
252
+ p2c := nativeLinearProductInto(relQ, kh, n, w.p2c)
253
+ offset := head * nativeHeadSize
254
+ for i := 0; i < n; i++ {
255
+ if ctx.Err() != nil {
256
+ return
257
+ }
258
+ row := scores[i*n : (i+1)*n]
259
+ largest := float32(-math.MaxFloat32)
260
+ for j := range row {
261
+ pos := positions[i*n+j]
262
+ // p2c gathers (-relative_pos), then transposes the query/key axes;
263
+ // for equal-length self attention this selects the same i-j bucket.
264
+ row[j] = row[j]/scale + (c2p[i*nativePositions+pos]/scale + p2c[j*nativePositions+pos]/scale)
265
+ if row[j] > largest {
266
+ largest = row[j]
267
+ }
268
+ }
269
+ var sum float32
270
+ for j := range row {
271
+ row[j] = float32(math.Exp(float64(row[j] - largest)))
272
+ sum += row[j]
273
+ }
274
+ for j := range row {
275
+ row[j] /= sum
276
+ }
277
+ }
278
+ vt := w.vt[:n*nativeHeadSize]
279
+ for i := 0; i < n; i++ {
280
+ for j := 0; j < nativeHeadSize; j++ {
281
+ vt[j*n+i] = vh[i*nativeHeadSize+j]
282
+ }
283
+ }
284
+ result := nativeLinearProductInto(prepareNativeLinearInto(nativeLinear{weight: vt, in: n, out: nativeHeadSize}, w.packedV), scores, n, w.result)
285
+ for i := 0; i < n; i++ {
286
+ copy(output[i*nativeHidden+offset:], result[i*nativeHeadSize:(i+1)*nativeHeadSize])
287
+ }
288
+ }
v3-ungated/native/stepsafety/native_weights.go ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "context"
5
+ "crypto/sha256"
6
+ "encoding/binary"
7
+ "encoding/hex"
8
+ "encoding/json"
9
+ "errors"
10
+ "fmt"
11
+ "io"
12
+ "math"
13
+ "slices"
14
+ )
15
+
16
+ // The native backend consumes the original float32 checkpoint, without
17
+ // quantization, conversion through ONNX, or any change to its learned weights.
18
+ const nativeWeightsSHA256 = "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1"
19
+ const nativeWeightsBytes = 283201512
20
+
21
+ type nativeTensor struct {
22
+ Shape []int
23
+ Data []float32
24
+ }
25
+
26
+ // readNativeWeights supports only the pinned checkpoint's F32 safetensors
27
+ // subset. Validate offsets and lengths before allocating, and verify the entire
28
+ // byte stream against the training pin before exposing any tensor to inference.
29
+ func readNativeWeights(ctx context.Context, source io.Reader) (map[string]nativeTensor, error) {
30
+ digest := sha256.New()
31
+ r := io.TeeReader(io.LimitReader(source, nativeWeightsBytes+1), digest)
32
+ var headerSize uint64
33
+ if err := binary.Read(r, binary.LittleEndian, &headerSize); err != nil {
34
+ return nil, err
35
+ }
36
+ if headerSize == 0 || headerSize > 128*1024 {
37
+ return nil, errors.New("invalid native model header size")
38
+ }
39
+ header := make([]byte, int(headerSize))
40
+ if _, err := io.ReadFull(r, header); err != nil {
41
+ return nil, err
42
+ }
43
+ var raw map[string]json.RawMessage
44
+ if err := json.Unmarshal(header, &raw); err != nil {
45
+ return nil, err
46
+ }
47
+ delete(raw, "__metadata__")
48
+ if len(raw) != 202 {
49
+ return nil, errors.New("unexpected native model tensor count")
50
+ }
51
+ type spec struct {
52
+ Name string
53
+ DType string `json:"dtype"`
54
+ Shape []int `json:"shape"`
55
+ Offsets [2]int64 `json:"data_offsets"`
56
+ }
57
+ entries := make([]spec, 0, len(raw))
58
+ for name, data := range raw {
59
+ entry := spec{Name: name}
60
+ if err := json.Unmarshal(data, &entry); err != nil {
61
+ return nil, err
62
+ }
63
+ size := int64(1)
64
+ if entry.DType != "F32" || len(entry.Shape) < 1 || len(entry.Shape) > 2 {
65
+ return nil, errors.New("unsupported native model tensor")
66
+ }
67
+ for _, dim := range entry.Shape {
68
+ if dim <= 0 || dim > 128005 {
69
+ return nil, errors.New("invalid native model dimension")
70
+ }
71
+ size *= int64(dim)
72
+ }
73
+ if entry.Offsets[0] < 0 || size*4 != entry.Offsets[1]-entry.Offsets[0] || entry.Offsets[1] > nativeWeightsBytes-int64(headerSize)-8 {
74
+ return nil, errors.New("invalid native model offsets")
75
+ }
76
+ entries = append(entries, entry)
77
+ }
78
+ slices.SortFunc(entries, func(a, b spec) int {
79
+ if a.Offsets[0] < b.Offsets[0] {
80
+ return -1
81
+ }
82
+ if a.Offsets[0] > b.Offsets[0] {
83
+ return 1
84
+ }
85
+ return 0
86
+ })
87
+ result := make(map[string]nativeTensor, len(entries))
88
+ buffer := make([]byte, 64*1024)
89
+ var offset int64
90
+ for _, entry := range entries {
91
+ if entry.Offsets[0] != offset {
92
+ return nil, errors.New("native model has overlapping or missing data")
93
+ }
94
+ values := make([]float32, (entry.Offsets[1]-offset)/4)
95
+ for start := 0; start < len(values); {
96
+ if err := ctx.Err(); err != nil {
97
+ return nil, err
98
+ }
99
+ n := min(len(buffer)/4, len(values)-start)
100
+ if _, err := io.ReadFull(r, buffer[:n*4]); err != nil {
101
+ return nil, err
102
+ }
103
+ for i := 0; i < n; i++ {
104
+ value := math.Float32frombits(binary.LittleEndian.Uint32(buffer[i*4:]))
105
+ if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
106
+ return nil, errors.New("non-finite native weight")
107
+ }
108
+ values[start+i] = value
109
+ }
110
+ start += n
111
+ }
112
+ result[entry.Name] = nativeTensor{Shape: entry.Shape, Data: values}
113
+ offset = entry.Offsets[1]
114
+ }
115
+ if n, err := r.Read(buffer[:1]); n != 0 || err != io.EOF || offset+int64(headerSize)+8 != nativeWeightsBytes {
116
+ return nil, errors.New("invalid native model length")
117
+ }
118
+ if hex.EncodeToString(digest.Sum(nil)) != nativeWeightsSHA256 {
119
+ return nil, errors.New("native model checksum mismatch")
120
+ }
121
+ return result, nil
122
+ }
123
+
124
+ func nativeWeight(tensors map[string]nativeTensor, name string, shape ...int) ([]float32, error) {
125
+ tensor, ok := tensors[name]
126
+ if !ok || !slices.Equal(tensor.Shape, shape) {
127
+ return nil, fmt.Errorf("unexpected native tensor %s", name)
128
+ }
129
+ return tensor.Data, nil
130
+ }
v3-ungated/native/stepsafety/native_workspace.go ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ // Inference reuses bounded scratch storage across layers and calls. In
4
+ // particular the six attention heads must not allocate 512x512 matrices on
5
+ // each of twelve layers. A pool also keeps independent callers race-free.
6
+ type nativeWorkspace struct {
7
+ x, q, k, v, a, attention, intermediate []float32
8
+ positions []int
9
+ heads [nativeHeads]nativeHeadWorkspace
10
+ }
11
+ type nativeHeadWorkspace struct {
12
+ q, k, v, vt, packedK, packedV, result []float32
13
+ scores, c2p, p2c []float32
14
+ }
15
+
16
+ func nativeBuffer(buffer []float32, n int) []float32 {
17
+ if cap(buffer) < n {
18
+ return make([]float32, n)
19
+ }
20
+ return buffer[:n]
21
+ }
22
+ func newNativeWorkspace() *nativeWorkspace {
23
+ h := func() []float32 { return make([]float32, maxSequenceLength*nativeHidden) }
24
+ w := &nativeWorkspace{x: h(), q: h(), k: h(), v: h(), a: h(), attention: h(), intermediate: make([]float32, maxSequenceLength*nativeIntermediate), positions: make([]int, maxSequenceLength*maxSequenceLength)}
25
+ for i := range w.heads {
26
+ s := func() []float32 { return make([]float32, maxSequenceLength*nativeHeadSize) }
27
+ matrix := func() []float32 { return make([]float32, maxSequenceLength*maxSequenceLength) }
28
+ w.heads[i] = nativeHeadWorkspace{q: s(), k: s(), v: s(), vt: s(), packedK: s(), packedV: s(), result: s(), scores: matrix(), c2p: matrix(), p2c: matrix()}
29
+ }
30
+ return w
31
+ }
v3-ungated/native/stepsafety/normalization.go ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "encoding/json"
5
+ "io"
6
+ "regexp"
7
+ "strings"
8
+ )
9
+
10
+ var schemaHeader = regexp.MustCompile(`(?m)^([A-Za-z_][A-Za-z0-9_.:/-]*):[ \t]+`)
11
+
12
+ // candidateFields matches the saved joint-CE input view. Only API binding names
13
+ // and JSON representation change; executable strings, argument keys, request and
14
+ // observation text remain intact. Never modify the hook's shared maps in place.
15
+ func candidateFields(input Input) ([4]string, error) {
16
+ var fields [4]string
17
+ schema, err := candidateSchema(input.AvailableToolSchemas)
18
+ if err != nil {
19
+ return fields, err
20
+ }
21
+ definitions := schemaDefinitions(schema)
22
+ spellings := map[string]map[string]bool{}
23
+ for _, d := range definitions {
24
+ name := d["name"].(string)
25
+ key := lowerASCII(name)
26
+ if spellings[key] == nil {
27
+ spellings[key] = map[string]bool{}
28
+ }
29
+ spellings[key][name] = true
30
+ }
31
+ name := func(s string) string {
32
+ if len(spellings[lowerASCII(s)]) > 1 {
33
+ return s
34
+ }
35
+ return lowerASCII(s)
36
+ }
37
+ for _, d := range definitions {
38
+ d["name"] = name(d["name"].(string))
39
+ }
40
+ var schemaText string
41
+ if schema != nil {
42
+ if text, ok := schema.(string); ok {
43
+ schemaText = text
44
+ } else {
45
+ schemaText, err = compactSortedJSON(schema)
46
+ if err != nil {
47
+ return fields, err
48
+ }
49
+ }
50
+ }
51
+ history := input.InteractionHistory
52
+ if strings.TrimSpace(history) == "" {
53
+ history = "[]"
54
+ }
55
+ h, err := decodeCandidateJSON(history)
56
+ if err != nil {
57
+ return fields, backendError(ErrorInvalidHistory, nil)
58
+ }
59
+ events, ok := h.([]any)
60
+ if !ok || len(events) > maxHistoryEntries {
61
+ return fields, backendError(ErrorInvalidHistory, nil)
62
+ }
63
+ for _, event := range events {
64
+ e, ok := event.(map[string]any)
65
+ if !ok {
66
+ return fields, backendError(ErrorInvalidHistory, nil)
67
+ }
68
+ t, ok := e["tool"].(string)
69
+ if !ok || t == "" {
70
+ return fields, backendError(ErrorInvalidHistory, nil)
71
+ }
72
+ e["tool"] = name(t)
73
+ }
74
+ history, err = compactSortedJSON(events)
75
+ if err != nil {
76
+ return fields, err
77
+ }
78
+ args := input.ToolArguments
79
+ // These exact interfaces use title/description as call-display metadata.
80
+ // A TaskCreate description, WebFetch prompt, or arbitrary API payload is data.
81
+ switch input.ToolName {
82
+ case "Bash", "exec_command", "functions.exec_command", "mcp__workspace__bash", "mcp__cua_repl__js":
83
+ if values, ok := args.(map[string]any); ok {
84
+ copy := make(map[string]any, len(values))
85
+ for k, v := range values {
86
+ if k != "title" && k != "description" {
87
+ copy[k] = v
88
+ }
89
+ }
90
+ args = copy
91
+ }
92
+ }
93
+ arguments, err := compactSortedJSON(args)
94
+ if err != nil {
95
+ return fields, err
96
+ }
97
+ return [4]string{input.UserRequest, history, strings.TrimSpace("[TOOL_NAME]\n" + name(input.ToolName) + "\n[ARGUMENTS]\n" + arguments), schemaText}, nil
98
+ }
99
+
100
+ func lowerASCII(s string) string {
101
+ return strings.Map(func(r rune) rune {
102
+ if r >= 'A' && r <= 'Z' {
103
+ return r + ('a' - 'A')
104
+ }
105
+ return r
106
+ }, s)
107
+ }
108
+
109
+ func decodeCandidateJSON(text string) (any, error) {
110
+ d := json.NewDecoder(strings.NewReader(text))
111
+ d.UseNumber()
112
+ var v any
113
+ if err := d.Decode(&v); err != nil {
114
+ return nil, err
115
+ }
116
+ var extra any
117
+ if err := d.Decode(&extra); err != io.EOF {
118
+ return nil, backendError(ErrorUnsupportedText, nil)
119
+ }
120
+ return v, nil
121
+ }
122
+
123
+ func candidateSchema(value any) (any, error) {
124
+ if value == nil {
125
+ return nil, nil
126
+ }
127
+ if text, ok := value.(string); ok {
128
+ if strings.TrimSpace(text) == "" {
129
+ return nil, nil
130
+ }
131
+ if parsed, err := decodeCandidateJSON(text); err == nil {
132
+ switch parsed.(type) {
133
+ case []any, map[string]any:
134
+ return parsed, nil
135
+ }
136
+ }
137
+ matches := schemaHeader.FindAllStringSubmatchIndex(text, -1)
138
+ if len(matches) > 0 && strings.TrimSpace(text[:matches[0][0]]) == "" {
139
+ var definitions []any
140
+ for i, m := range matches {
141
+ end := len(text)
142
+ if i+1 < len(matches) {
143
+ end = matches[i+1][0]
144
+ }
145
+ definitions = append(definitions, map[string]any{"name": text[m[2]:m[3]], "description": strings.TrimSpace(text[m[1]:end])})
146
+ }
147
+ return definitions, nil
148
+ }
149
+ // Research also accepted Python literal containers. Hooks supply JSON;
150
+ // abstain on such unsupported containers instead of guessing a conversion.
151
+ if strings.HasPrefix(strings.TrimSpace(text), "[") || strings.HasPrefix(strings.TrimSpace(text), "{") {
152
+ return nil, backendError(ErrorUnsupportedText, nil)
153
+ }
154
+ return text, nil
155
+ }
156
+ text, err := compactSortedJSON(value)
157
+ if err != nil {
158
+ return nil, err
159
+ }
160
+ return decodeCandidateJSON(text)
161
+ }
162
+
163
+ func schemaDefinitions(value any) []map[string]any {
164
+ switch v := value.(type) {
165
+ case []any:
166
+ var result []map[string]any
167
+ for _, item := range v {
168
+ result = append(result, schemaDefinitions(item)...)
169
+ }
170
+ return result
171
+ case map[string]any:
172
+ if f, ok := v["function"].(map[string]any); ok {
173
+ return schemaDefinitions(f)
174
+ }
175
+ if t, ok := v["tools"].([]any); ok {
176
+ return schemaDefinitions(t)
177
+ }
178
+ _, named := v["name"].(string)
179
+ _, parameters := v["parameters"]
180
+ _, inputSchema := v["input_schema"]
181
+ _, description := v["description"]
182
+ if named && (parameters || inputSchema || description || len(v) == 1) {
183
+ return []map[string]any{v}
184
+ }
185
+ }
186
+ return nil
187
+ }
v3-ungated/native/stepsafety/packing.go ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "strings"
8
+ )
9
+
10
+ const maxSequenceLength = 512
11
+
12
+ var fieldBudgets = [4]int{96, 144, 128, 128}
13
+ var fieldMarkerIDs = [4]int64{128001, 128002, 128003, 128004}
14
+
15
+ type tokenEncoder interface {
16
+ encode(context.Context, string) ([]int64, error)
17
+ }
18
+ type packedInput struct {
19
+ IDs []int64
20
+ Mask []int64
21
+ HistoryOmitted bool
22
+ }
23
+
24
+ func packInput(ctx context.Context, tokenizer tokenEncoder, input Input) (packedInput, error) {
25
+ var result packedInput
26
+ if ExcludedTool(input.ToolName) {
27
+ return result, backendError(ErrorExcludedTool, nil)
28
+ }
29
+ if err := validateInputBounds(input); err != nil {
30
+ return result, err
31
+ }
32
+ fields, err := candidateFields(input)
33
+ if err != nil {
34
+ return result, err
35
+ }
36
+ var selected [4][]int64
37
+ for _, index := range []int{2, 0, 3} {
38
+ tokens, err := tokenizer.encode(ctx, fields[index])
39
+ if err != nil {
40
+ return result, err
41
+ }
42
+ if len(tokens) > fieldBudgets[index] {
43
+ code := ErrorActionTooLarge
44
+ if index == 0 {
45
+ code = ErrorRequestTooLarge
46
+ }
47
+ if index == 3 {
48
+ code = ErrorSchemaTooLarge
49
+ }
50
+ return result, backendError(code, nil)
51
+ }
52
+ selected[index] = tokens
53
+ }
54
+ selected[1], result.HistoryOmitted, err = selectHistory(ctx, tokenizer, fields[1], fieldBudgets[1])
55
+ if err != nil {
56
+ return result, err
57
+ }
58
+ result.HistoryOmitted = result.HistoryOmitted || input.HistoryOmitted
59
+ result.IDs, result.Mask = assembleTokens(selected)
60
+ return result, nil
61
+ }
62
+
63
+ // Keep the training field order, special IDs, separators, and padding. Only
64
+ // history is truncated; admitted requests, actions, and schemas stay complete.
65
+ func assembleTokens(fields [4][]int64) ([]int64, []int64) {
66
+ ids := make([]int64, 0, maxSequenceLength)
67
+ ids = append(ids, 1)
68
+ for i, tokens := range fields {
69
+ ids = append(ids, fieldMarkerIDs[i])
70
+ ids = append(ids, tokens...)
71
+ ids = append(ids, 2)
72
+ }
73
+ mask := make([]int64, maxSequenceLength)
74
+ for i := range ids {
75
+ mask[i] = 1
76
+ }
77
+ ids = append(ids, make([]int64, maxSequenceLength-len(ids))...)
78
+ return ids, mask
79
+ }
80
+
81
+ func selectHistory(ctx context.Context, tokenizer tokenEncoder, history string, budget int) ([]int64, bool, error) {
82
+ if strings.TrimSpace(history) == "" {
83
+ history = "[]"
84
+ }
85
+ var entries []json.RawMessage
86
+ if err := json.Unmarshal([]byte(history), &entries); err != nil {
87
+ return nil, false, backendError(ErrorInvalidHistory, nil)
88
+ }
89
+ if len(entries) > maxHistoryEntries {
90
+ return nil, false, backendError(ErrorInputTooLarge, nil)
91
+ }
92
+ selected := make([]json.RawMessage, 0, len(entries))
93
+ omitted := false
94
+ for _, entry := range entries {
95
+ var event struct {
96
+ Tool string `json:"tool"`
97
+ }
98
+ if err := json.Unmarshal(entry, &event); err != nil || event.Tool == "" {
99
+ return nil, omitted, backendError(ErrorInvalidHistory, nil)
100
+ }
101
+ if ExcludedTool(event.Tool) {
102
+ omitted = true
103
+ continue
104
+ }
105
+ selected = append(selected, entry)
106
+ }
107
+ encoded, err := compactSortedJSON(selected)
108
+ if err != nil {
109
+ return nil, omitted, err
110
+ }
111
+ tokens, err := tokenizer.encode(ctx, encoded)
112
+ if err != nil {
113
+ return nil, omitted, err
114
+ }
115
+ if len(tokens) > budget {
116
+ // Match the training history packer after removing excluded tools:
117
+ // first 72 + last 72 tokens at the fixed 144-token history budget.
118
+ // The model consumes token fragments; they need not form valid JSON.
119
+ head := (budget + 1) / 2
120
+ tail := budget - head
121
+ truncated := make([]int64, 0, budget)
122
+ tokens = append(append(truncated, tokens[:head]...), tokens[len(tokens)-tail:]...)
123
+ omitted = true
124
+ }
125
+ return tokens, omitted, nil
126
+ }
127
+
128
+ // The schema field used Python's default JSON separators (unlike action and
129
+ // history). Insert spaces outside strings only to preserve that contract.
130
+ func schemaText(value any) (string, error) {
131
+ if value == nil {
132
+ return "", nil
133
+ }
134
+ if text, ok := value.(string); ok {
135
+ return text, nil
136
+ }
137
+ compact, err := compactSortedJSON(value)
138
+ if err != nil {
139
+ return "", err
140
+ }
141
+ var out strings.Builder
142
+ quoted, escaped := false, false
143
+ for _, r := range compact {
144
+ out.WriteRune(r)
145
+ if quoted {
146
+ if escaped {
147
+ escaped = false
148
+ } else if r == '\\' {
149
+ escaped = true
150
+ } else if r == '"' {
151
+ quoted = false
152
+ }
153
+ continue
154
+ }
155
+ if r == '"' {
156
+ quoted = true
157
+ } else if r == ':' || r == ',' {
158
+ out.WriteByte(' ')
159
+ }
160
+ }
161
+ return out.String(), nil
162
+ }
163
+
164
+ // Match known file-tool names and namespace leaves, without excluding broad
165
+ // verbs such as read_email or write_message that the model can still score.
166
+ func ExcludedTool(name string) bool {
167
+ if len(name) > 256 {
168
+ return false
169
+ }
170
+ name = strings.ToLower(strings.TrimSpace(name))
171
+ if i := strings.LastIndex(name, "__"); i >= 0 {
172
+ name = name[i+2:]
173
+ }
174
+ if i := strings.LastIndexAny(name, ".:/"); i >= 0 {
175
+ name = name[i+1:]
176
+ }
177
+ switch name {
178
+ case "read", "write", "edit", "multiedit", "notebookedit", "apply_patch", "read_file", "read_text_file", "read_multiple_files", "write_file", "edit_file", "replace_file_content", "multi_replace_file_content":
179
+ return true
180
+ }
181
+ return false
182
+ }
183
+
184
+ // Reject large or malformed JSON values before allocating their serialization.
185
+ // Hook adapters supply JSON primitives; unsupported Go objects aren't inputs.
186
+ func boundedJSONSize(value any, remaining *int, depth int) error {
187
+ if depth > 64 || *remaining < 0 {
188
+ return backendError(ErrorInputTooLarge, nil)
189
+ }
190
+ *remaining -= 2
191
+ switch v := value.(type) {
192
+ case nil, bool, float64, float32, int, int64, json.Number:
193
+ case string:
194
+ *remaining -= len(v)
195
+ case map[string]any:
196
+ for key, item := range v {
197
+ *remaining -= len(key) + 3
198
+ if err := boundedJSONSize(item, remaining, depth+1); err != nil {
199
+ return err
200
+ }
201
+ }
202
+ case []any:
203
+ for _, item := range v {
204
+ if err := boundedJSONSize(item, remaining, depth+1); err != nil {
205
+ return err
206
+ }
207
+ }
208
+ default:
209
+ return backendError(ErrorInference, errors.New("unsupported JSON input"))
210
+ }
211
+ if *remaining < 0 {
212
+ return backendError(ErrorInputTooLarge, nil)
213
+ }
214
+ return nil
215
+ }
v3-ungated/native/stepsafety/request_gate.go ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "compress/gzip"
5
+ "context"
6
+ "crypto/sha256"
7
+ "encoding/hex"
8
+ "encoding/json"
9
+ "errors"
10
+ "golang.org/x/text/cases"
11
+ "golang.org/x/text/language"
12
+ "io"
13
+ "math"
14
+ "strings"
15
+ "unicode"
16
+ )
17
+
18
+ const ErrorRequestNotInformational = "request_not_informational"
19
+
20
+ var requestGateModel struct {
21
+ Word, Char sparseVector
22
+ Intercept, Threshold float64
23
+ Calibrator scoreCalibration
24
+ }
25
+
26
+ func loadRequestGateAssets() error {
27
+ f, e := nativeAssets.Open("model/native/request_gate.json.gz")
28
+ if e != nil {
29
+ return e
30
+ }
31
+ defer f.Close()
32
+ r, e := gzip.NewReader(f)
33
+ if e != nil {
34
+ return e
35
+ }
36
+ defer r.Close()
37
+ data, e := io.ReadAll(io.LimitReader(r, 5259026+1))
38
+ if e != nil {
39
+ return e
40
+ }
41
+ h := sha256.Sum256(data)
42
+ if len(data) != 5259026 || hex.EncodeToString(h[:]) != "d94b34f3c59803047365b594fe50531dd073d8ac14b5b8151c574b61d647f693" {
43
+ return errors.New("request gate checksum mismatch")
44
+ }
45
+ return json.Unmarshal(data, &requestGateModel)
46
+ }
47
+ func requestIntentProbability(text string) float64 {
48
+ c := requestGateModel.Calibrator
49
+ z := c.Coefficient*requestIntentMargin(text) + c.Intercept
50
+ if z >= 0 {
51
+ return 1 / (1 + math.Exp(-z))
52
+ }
53
+ v := math.Exp(z)
54
+ return v / (1 + v)
55
+ }
56
+ func requestIntentGate(ctx context.Context, text string) error {
57
+ if e := ctx.Err(); e != nil {
58
+ return e
59
+ }
60
+ p := requestIntentProbability(text)
61
+ if e := ctx.Err(); e != nil {
62
+ return e
63
+ }
64
+ if p < requestGateModel.Threshold {
65
+ return backendError(ErrorRequestNotInformational, nil)
66
+ }
67
+ return nil
68
+ }
69
+
70
+ func requestIntentMargin(text string) float64 {
71
+ // Match sklearn's lowercase Unicode word analyzer and char analyzer.
72
+ // The vocabulary is hashed to avoid shipping plaintext captured tokens.
73
+ text = cases.Lower(language.Und).String(text)
74
+ words := []string{}
75
+ run := []rune{}
76
+ flush := func() {
77
+ if len(run) >= 2 {
78
+ words = append(words, string(run))
79
+ }
80
+ run = run[:0]
81
+ }
82
+ for _, r := range text {
83
+ if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
84
+ run = append(run, r)
85
+ } else {
86
+ flush()
87
+ }
88
+ }
89
+ flush()
90
+ counts := map[string]int{}
91
+ for i, word := range words {
92
+ counts[word]++
93
+ if i > 0 {
94
+ counts[words[i-1]+" "+word]++
95
+ }
96
+ }
97
+ score := requestGateModel.Intercept + vectorScore(counts, requestGateModel.Word)
98
+ var normalized strings.Builder
99
+ rr := []rune(text)
100
+ for i := 0; i < len(rr); {
101
+ j := i + 1
102
+ if pythonSpace(rr[i]) {
103
+ for j < len(rr) && pythonSpace(rr[j]) {
104
+ j++
105
+ }
106
+ }
107
+ if j-i >= 2 {
108
+ normalized.WriteByte(' ')
109
+ } else {
110
+ normalized.WriteRune(rr[i])
111
+ }
112
+ i = j
113
+ }
114
+ rr = []rune(normalized.String())
115
+ counts = map[string]int{}
116
+ for n := 3; n <= 5; n++ {
117
+ for i := 0; i+n <= len(rr); i++ {
118
+ counts[string(rr[i:i+n])]++
119
+ }
120
+ }
121
+ return score + vectorScore(counts, requestGateModel.Char)
122
+ }
v3-ungated/native/stepsafety/runtime_artifacts.go ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ // Official microsoft/onnxruntime release assets. Archive hashes are the
4
+ // publisher's GitHub asset digests; library hashes pin the extracted bytes.
5
+ // Intel macOS uses 1.23.2, the last upstream x86_64 release asset.
6
+ var runtimeArtifacts = map[string]runtimeSpec{
7
+ "darwin/arm64": {Version: "1.24.4", Archive: artifactSpec{Name: "onnxruntime-osx-arm64-1.24.4.tgz", Size: 30937282, SHA256: "93787795f47e1eee369182e43ed51b9e5da0878ab0346aecf4258979b8bba989"}, Library: artifactSpec{Name: "libonnxruntime.1.24.4.dylib", Size: 35418600, SHA256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"}},
8
+ "darwin/amd64": {Version: "1.23.2", Archive: artifactSpec{Name: "onnxruntime-osx-x86_64-1.23.2.tgz", Size: 11676322, SHA256: "d10359e16347b57d9959f7e80a225a5b4a66ed7d7e007274a15cae86836485a6"}, Library: artifactSpec{Name: "libonnxruntime.1.23.2.dylib", Size: 39742608, SHA256: "8c9c78de65ea3786f987c0d980e9c1b13a3a5fbc6b3e2965ba05b450e6e4c054"}},
9
+ "linux/arm64": {Version: "1.24.4", Archive: artifactSpec{Name: "onnxruntime-linux-aarch64-1.24.4.tgz", Size: 7181958, SHA256: "866109a9248d057671a039b9d725be4bd86888e3754140e6701ec621be9d4d7e"}, Library: artifactSpec{Name: "libonnxruntime.so.1.24.4", Size: 18625376, SHA256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"}},
10
+ "linux/amd64": {Version: "1.24.4", Archive: artifactSpec{Name: "onnxruntime-linux-x64-1.24.4.tgz", Size: 8155822, SHA256: "3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747"}, Library: artifactSpec{Name: "libonnxruntime.so.1.24.4", Size: 22159232, SHA256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"}},
11
+ }
v3-ungated/native/stepsafety/runtime_store.go ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "archive/tar"
5
+ "compress/gzip"
6
+ "context"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "io"
11
+ "net/http"
12
+ "os"
13
+ "path/filepath"
14
+ "runtime"
15
+ "time"
16
+ )
17
+
18
+ type runtimeSpec struct {
19
+ Version string
20
+ Archive artifactSpec
21
+ Library artifactSpec
22
+ }
23
+
24
+ func runtimeForPlatform() (runtimeSpec, error) {
25
+ spec, ok := runtimeArtifacts[runtime.GOOS+"/"+runtime.GOARCH]
26
+ if !ok {
27
+ return runtimeSpec{}, errors.New("step-safety ONNX Runtime is unsupported on this platform")
28
+ }
29
+ return spec, nil
30
+ }
31
+
32
+ func runtimeDir(modelDir string) string {
33
+ return filepath.Join(modelDir, "runtime", runtime.GOOS+"-"+runtime.GOARCH)
34
+ }
35
+
36
+ func ValidateRuntimeDir(modelDir string) (string, error) {
37
+ spec, err := runtimeForPlatform()
38
+ if err != nil {
39
+ return "", err
40
+ }
41
+ path := filepath.Join(runtimeDir(modelDir), spec.Library.Name)
42
+ if err := verifyArtifact(path, spec.Library); err != nil {
43
+ return "", err
44
+ }
45
+ return path, nil
46
+ }
47
+
48
+ func verifyArtifact(path string, spec artifactSpec) error {
49
+ info, err := os.Stat(path)
50
+ if err != nil {
51
+ return fmt.Errorf("artifact %s is unavailable: %w", spec.Name, err)
52
+ }
53
+ if !info.Mode().IsRegular() || info.Size() != spec.Size {
54
+ return fmt.Errorf("artifact %s has unexpected size", spec.Name)
55
+ }
56
+ hash, err := fileSHA256(path)
57
+ if err != nil || hash != spec.SHA256 {
58
+ return fmt.Errorf("artifact %s failed checksum verification", spec.Name)
59
+ }
60
+ return nil
61
+ }
62
+
63
+ // InstallRuntime is called only by the explicit install command. Daemon
64
+ // startup is strictly offline and loads only this checksum-pinned library.
65
+ // archivePath supports air-gapped installation; an empty path downloads the
66
+ // platform-specific CPU archive from Microsoft's official GitHub release.
67
+ func InstallRuntime(ctx context.Context, modelDir, archivePath string) error {
68
+ spec, err := runtimeForPlatform()
69
+ if err != nil {
70
+ return err
71
+ }
72
+ destination := runtimeDir(modelDir)
73
+ if _, err := ValidateRuntimeDir(modelDir); err == nil {
74
+ return nil
75
+ }
76
+ if _, err := os.Stat(destination); err == nil {
77
+ return fmt.Errorf("ONNX Runtime destination exists but is invalid: %s", destination)
78
+ } else if !errors.Is(err, os.ErrNotExist) {
79
+ return err
80
+ }
81
+ parent := filepath.Dir(destination)
82
+ if err := os.MkdirAll(parent, 0o700); err != nil {
83
+ return err
84
+ }
85
+ temporary, err := os.MkdirTemp(parent, ".onnx-install-*")
86
+ if err != nil {
87
+ return err
88
+ }
89
+ defer os.RemoveAll(temporary)
90
+ if archivePath == "" {
91
+ archivePath = filepath.Join(temporary, spec.Archive.Name)
92
+ if err := downloadRuntime(ctx, spec, archivePath); err != nil {
93
+ return err
94
+ }
95
+ }
96
+ if err := verifyArtifact(archivePath, spec.Archive); err != nil {
97
+ return err
98
+ }
99
+ staged := filepath.Join(temporary, "runtime")
100
+ if err := os.Mkdir(staged, 0o700); err != nil {
101
+ return err
102
+ }
103
+ if err := extractRuntime(archivePath, staged, spec); err != nil {
104
+ return err
105
+ }
106
+ if err := verifyArtifact(filepath.Join(staged, spec.Library.Name), spec.Library); err != nil {
107
+ return err
108
+ }
109
+ provenance, err := json.MarshalIndent(spec, "", " ")
110
+ if err != nil {
111
+ return err
112
+ }
113
+ if err := os.WriteFile(filepath.Join(staged, "PROVENANCE.json"), append(provenance, '\n'), 0o600); err != nil {
114
+ return err
115
+ }
116
+ if err := ctx.Err(); err != nil {
117
+ return err
118
+ }
119
+ return os.Rename(staged, destination)
120
+ }
121
+
122
+ func downloadRuntime(ctx context.Context, spec runtimeSpec, path string) error {
123
+ url := "https://github.com/microsoft/onnxruntime/releases/download/v" + spec.Version + "/" + spec.Archive.Name
124
+ request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
125
+ if err != nil {
126
+ return err
127
+ }
128
+ client := &http.Client{Timeout: 2 * time.Minute}
129
+ response, err := client.Do(request)
130
+ if err != nil {
131
+ return err
132
+ }
133
+ defer response.Body.Close()
134
+ if response.StatusCode != http.StatusOK {
135
+ return fmt.Errorf("ONNX Runtime download returned HTTP %d", response.StatusCode)
136
+ }
137
+ out, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
138
+ if err != nil {
139
+ return err
140
+ }
141
+ written, copyErr := io.Copy(out, io.LimitReader(response.Body, spec.Archive.Size+1))
142
+ closeErr := out.Close()
143
+ if err := errors.Join(copyErr, closeErr); err != nil {
144
+ return err
145
+ }
146
+ if written != spec.Archive.Size {
147
+ return errors.New("ONNX Runtime download has unexpected size")
148
+ }
149
+ return nil
150
+ }
151
+
152
+ func extractRuntime(archivePath, destination string, spec runtimeSpec) error {
153
+ file, err := os.Open(archivePath)
154
+ if err != nil {
155
+ return err
156
+ }
157
+ defer file.Close()
158
+ zipped, err := gzip.NewReader(file)
159
+ if err != nil {
160
+ return err
161
+ }
162
+ defer zipped.Close()
163
+ archive := tar.NewReader(zipped)
164
+ root := spec.Archive.Name[:len(spec.Archive.Name)-len(".tgz")]
165
+ // Explicit allowlist: never extract archive paths or follow its symlinks.
166
+ wanted := map[string]string{
167
+ root + "/lib/" + spec.Library.Name: spec.Library.Name,
168
+ root + "/LICENSE": "LICENSE",
169
+ root + "/ThirdPartyNotices.txt": "ThirdPartyNotices.txt",
170
+ }
171
+ for {
172
+ header, err := archive.Next()
173
+ if errors.Is(err, io.EOF) {
174
+ break
175
+ }
176
+ if err != nil {
177
+ return err
178
+ }
179
+ name, ok := wanted[header.Name]
180
+ if !ok {
181
+ continue
182
+ }
183
+ limit := int64(4 * 1024 * 1024)
184
+ if name == spec.Library.Name {
185
+ limit = spec.Library.Size
186
+ }
187
+ if header.Typeflag != tar.TypeReg || header.Size < 1 || header.Size > limit {
188
+ return errors.New("unexpected ONNX Runtime archive entry")
189
+ }
190
+ out, err := os.OpenFile(filepath.Join(destination, name), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
191
+ if err != nil {
192
+ return err
193
+ }
194
+ _, copyErr := io.Copy(out, archive)
195
+ if err := errors.Join(copyErr, out.Close()); err != nil {
196
+ return err
197
+ }
198
+ delete(wanted, header.Name)
199
+ }
200
+ if len(wanted) != 0 {
201
+ return errors.New("ONNX Runtime archive is missing required library or license files")
202
+ }
203
+ return nil
204
+ }
v3-ungated/native/stepsafety/scoped.go ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "math"
7
+ "regexp"
8
+ "strings"
9
+ )
10
+
11
+ const ErrorUnsupportedEffect = "unsupported_effect"
12
+ const ErrorMissingRequest = "missing_request"
13
+ const ErrorCompoundRequest = "compound_or_mutation_request"
14
+
15
+ var scopeCamel = regexp.MustCompile(`([a-z0-9])([A-Z])`)
16
+ var scopeWords = regexp.MustCompile(`[a-z0-9]+`)
17
+ var requestWords = regexp.MustCompile(`[a-z]+`)
18
+
19
+ func scopeHas(words []string, options string) bool {
20
+ for _, option := range strings.Fields(options) {
21
+ for _, w := range words {
22
+ if w == option {
23
+ return true
24
+ }
25
+ }
26
+ }
27
+ return false
28
+ }
29
+ func scopePopulated(value any) bool {
30
+ switch v := value.(type) {
31
+ case string:
32
+ return strings.TrimSpace(v) != ""
33
+ case float64:
34
+ return !math.IsNaN(v) && !math.IsInf(v, 0)
35
+ case float32:
36
+ return !math.IsNaN(float64(v)) && !math.IsInf(float64(v), 0)
37
+ case int, int64:
38
+ return true
39
+ case json.Number:
40
+ n, e := v.Float64()
41
+ return e == nil && !math.IsNaN(n) && !math.IsInf(n, 0)
42
+ case []any:
43
+ if len(v) == 0 {
44
+ return false
45
+ }
46
+ for _, x := range v {
47
+ if !scopePopulated(x) {
48
+ return false
49
+ }
50
+ }
51
+ return true
52
+ }
53
+ return false
54
+ }
55
+ func recognizedScopeEffect(tool string, raw any) string {
56
+ args, ok := raw.(map[string]any)
57
+ if !ok {
58
+ return ""
59
+ }
60
+ ws := scopeWords.FindAllString(strings.ToLower(scopeCamel.ReplaceAllString(tool, "${1} ${2}")), -1)
61
+ if scopeHas(ws, "search find get list read inspect describe preview dry simulate check test") {
62
+ return ""
63
+ }
64
+ for _, key := range []string{"dry_run", "dryRun", "preview", "simulate"} {
65
+ if v, ok := args[key].(bool); ok && v {
66
+ return ""
67
+ }
68
+ }
69
+ keys := []string{}
70
+ for k, v := range args {
71
+ if scopePopulated(v) {
72
+ keys = append(keys, strings.ToLower(k))
73
+ }
74
+ }
75
+ if scopeHas(ws, "delete remove erase truncate drop wipe unlink") && scopeHas(ws, "file files email emails message messages record records database databases directory directories bucket buckets") && scopeHas(keys, "id ids file_id email_id message_id record_id path file filename name database bucket target records directory") {
76
+ return "destructive_object_change"
77
+ }
78
+ if scopeHas(ws, "send transfer pay") && scopeHas(ws, "money payment payments funds") && scopeHas(keys, "recipient to account destination recipient_id account_id") {
79
+ var amount float64
80
+ switch n := args["amount"].(type) {
81
+ case float64:
82
+ amount = n
83
+ case float32:
84
+ amount = float64(n)
85
+ case int:
86
+ amount = float64(n)
87
+ case int64:
88
+ amount = float64(n)
89
+ case json.Number:
90
+ amount, _ = n.Float64()
91
+ }
92
+ if amount > 0 && !math.IsNaN(amount) && !math.IsInf(amount, 0) {
93
+ return "money_transfer"
94
+ }
95
+ }
96
+ if scopeHas(ws, "change reset update set rotate") && scopeHas(ws, "password passwords credential credentials") {
97
+ for _, key := range []string{"password", "new_password", "newPassword", "credential"} {
98
+ if v, ok := args[key].(string); ok && strings.TrimSpace(v) != "" {
99
+ return "credential_change"
100
+ }
101
+ }
102
+ }
103
+ if scopeHas(ws, "send post publish forward") && scopeHas(ws, "email message mail") && scopeHas(keys, "recipient recipients to channel destination") && scopeHas(keys, "body content message text") {
104
+ return "outgoing_message"
105
+ }
106
+ if scopeHas(ws, "share grant invite") && scopeHas(ws, "file document folder access permission") && scopeHas(keys, "recipient recipients user users email") && scopeHas(keys, "file_id document_id folder_id path resource permission") {
107
+ return "access_share"
108
+ }
109
+ return ""
110
+ }
111
+
112
+ func scopedEligibility(input Input) error {
113
+ if recognizedScopeEffect(input.ToolName, input.ToolArguments) == "" {
114
+ return backendError(ErrorUnsupportedEffect, nil)
115
+ }
116
+ if strings.TrimSpace(input.UserRequest) == "" {
117
+ return backendError(ErrorMissingRequest, nil)
118
+ }
119
+ if scopeHas(requestWords.FindAllString(strings.ToLower(input.UserRequest), -1), "delete remove erase transfer send change update pay create modify write add book reserve schedule cancel install run execute deploy reset adjust approve archive buy checkout clear commit disburse discard dispose drop empty forward grant invite merge move order post publish purchase purge push refund reimburse rename reply reschedule respond restart rotate save set share start stop submit unlink upload wipe") {
120
+ return backendError(ErrorCompoundRequest, nil)
121
+ }
122
+ return nil
123
+ }
124
+
125
+ // packScopedInput preserves the frozen bounded contextual representation.
126
+ // The current action is complete. Request uses its head, history and schema
127
+ // use head/tail. Eligibility is independent of tool effect and request intent.
128
+ // V3 ungated retains the full-action limit and action-agreement scoring.
129
+ func packScopedInput(ctx context.Context, tok tokenEncoder, input Input) (packedInput, error) {
130
+ var out packedInput
131
+ if err := validateInputBounds(input); err != nil {
132
+ return out, err
133
+ }
134
+ fields, err := candidateFields(input)
135
+ if err != nil {
136
+ return out, err
137
+ }
138
+ var selected [4][]int64
139
+ for i, text := range fields {
140
+ tokens, err := tok.encode(ctx, text)
141
+ if err != nil {
142
+ return out, err
143
+ }
144
+ budget := fieldBudgets[i]
145
+ if len(tokens) > budget {
146
+ if i == 2 {
147
+ return out, backendError(ErrorActionTooLarge, nil)
148
+ }
149
+ if i == 0 {
150
+ tokens = tokens[:budget]
151
+ } else {
152
+ head := (budget + 1) / 2
153
+ tokens = append(append([]int64{}, tokens[:head]...), tokens[len(tokens)-(budget-head):]...)
154
+ }
155
+ if i == 1 {
156
+ out.HistoryOmitted = true
157
+ }
158
+ }
159
+ selected[i] = tokens
160
+ }
161
+ out.IDs, out.Mask = assembleTokens(selected)
162
+ out.HistoryOmitted = out.HistoryOmitted || input.HistoryOmitted
163
+ return out, nil
164
+ }
v3-ungated/native/stepsafety/tokenizer.go ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "context"
5
+ "os"
6
+ "strings"
7
+ "unicode"
8
+ "unicode/utf8"
9
+
10
+ "golang.org/x/text/unicode/norm"
11
+ )
12
+
13
+ // This adapter is deliberately specific to the checksum-pinned DeBERTa
14
+ // tokenizer: the HF Replace -> NFC -> Strip normalizer and added special
15
+ // tokens here, Metaspace and Unigram segmentation in unigram.go.
16
+ type tokenizer struct{ unigram *unigram }
17
+
18
+ var specialTokens = []string{"[PAD]", "[CLS]", "[SEP]", "[UNK]", "[MASK]", "[USER_REQUEST]", "[INTERACTION_HISTORY]", "[CURRENT_ACTION]", "[TOOL_DESCRIPTIONS]"}
19
+ var specialTokenIDs = []int64{0, 1, 2, 3, 128000, 128001, 128002, 128003, 128004}
20
+
21
+ func loadTokenizer(path string) (*tokenizer, error) {
22
+ data, err := os.ReadFile(path)
23
+ if err != nil {
24
+ return nil, err
25
+ }
26
+ return parseTokenizer(data)
27
+ }
28
+
29
+ func parseTokenizer(data []byte) (*tokenizer, error) {
30
+ unigram, err := parseUnigram(data)
31
+ if err != nil {
32
+ return nil, err
33
+ }
34
+ return &tokenizer{unigram: unigram}, nil
35
+ }
36
+
37
+ func (t *tokenizer) encode(ctx context.Context, text string) ([]int64, error) {
38
+ var ids []int64
39
+ for len(text) > 0 {
40
+ if err := ctx.Err(); err != nil {
41
+ return nil, err
42
+ }
43
+ next, special := len(text), -1
44
+ for i, token := range specialTokens {
45
+ if at := strings.Index(text, token); at >= 0 && at < next {
46
+ next, special = at, i
47
+ }
48
+ }
49
+ // Added special tokens are extracted before normalizing each ordinary
50
+ // segment, matching normalized=false and lstrip/rstrip=false in HF.
51
+ segment := text[:next]
52
+ normalized := normalizeText(segment)
53
+ // Go NFC inserts CGJ for non-stream-safe combining sequences; HF NFC
54
+ // does not. Abstain on those rare inputs rather than silently changing
55
+ // the representation the checkpoint was trained on.
56
+ if strings.Count(normalized, "\u034f") != strings.Count(segment, "\u034f") {
57
+ return nil, backendError(ErrorUnsupportedText, nil)
58
+ }
59
+ ids = t.unigram.encode(normalized, ids)
60
+ if special < 0 {
61
+ break
62
+ }
63
+ ids = append(ids, specialTokenIDs[special])
64
+ text = text[next+len(specialTokens[special]):]
65
+ }
66
+ return ids, ctx.Err()
67
+ }
68
+
69
+ func normalizeText(text string) string {
70
+ var out strings.Builder
71
+ out.Grow(len(text))
72
+ for i := 0; i < len(text); {
73
+ r, size := utf8.DecodeRuneInString(text[i:])
74
+ if !unicode.IsSpace(r) {
75
+ out.WriteRune(r)
76
+ i += size
77
+ continue
78
+ }
79
+ start, count := i, 0
80
+ for i < len(text) {
81
+ r, size = utf8.DecodeRuneInString(text[i:])
82
+ if !unicode.IsSpace(r) {
83
+ break
84
+ }
85
+ count++
86
+ i += size
87
+ }
88
+ whitespace := text[start:i]
89
+ // Rust regex \s{2,}|[\n\r\t] uses Unicode whitespace; Go's regexp \s
90
+ // is ASCII-only, so using it here would silently change token IDs.
91
+ if count >= 2 || whitespace == "\n" || whitespace == "\r" || whitespace == "\t" {
92
+ out.WriteByte(' ')
93
+ } else {
94
+ out.WriteString(whitespace)
95
+ }
96
+ }
97
+ return strings.TrimRightFunc(nfcUnicode9(out.String()), unicode.IsSpace)
98
+ }
99
+
100
+ // nfcUnicode9 matches HF NFC, whose tables are Unicode 9.0. HF treats every
101
+ // later character as an unassigned starter that never reorders or composes,
102
+ // while x/text's newer tables move or merge the ones in postUnicode9NFC. NFC
103
+ // cannot act across such a starter, so normalizing the text between them and
104
+ // copying them unchanged reproduces HF.
105
+ func nfcUnicode9(text string) string {
106
+ var out strings.Builder
107
+ for {
108
+ i := strings.IndexFunc(text, func(r rune) bool { return unicode.Is(postUnicode9NFC, r) })
109
+ if i < 0 {
110
+ break
111
+ }
112
+ _, size := utf8.DecodeRuneInString(text[i:])
113
+ out.WriteString(norm.NFC.String(text[:i]))
114
+ out.WriteString(text[i : i+size])
115
+ text = text[i+size:]
116
+ }
117
+ if out.Len() == 0 {
118
+ return norm.NFC.String(text)
119
+ }
120
+ out.WriteString(norm.NFC.String(text))
121
+ return out.String()
122
+ }
v3-ungated/native/stepsafety/types.go ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Package stepsafety runs the local, observe-only step-level tool safety pilot.
2
+ //
3
+ // The package deliberately has no enforcement API. A result is evidence for
4
+ // review, never an authorization decision, and every model failure degrades to
5
+ // an unavailable shadow result instead of an error returned to the hook path.
6
+ package stepsafety
7
+
8
+ import (
9
+ "context"
10
+ "encoding/json"
11
+ "errors"
12
+ "fmt"
13
+ "math"
14
+ "os"
15
+ "strconv"
16
+ "strings"
17
+ "time"
18
+ )
19
+
20
+ const (
21
+ ModelVersion = "merlin-v3-ungated-20261104"
22
+ Threshold = 0.5
23
+
24
+ calibrationScale = 1.0
25
+ calibrationBias = 0.0
26
+
27
+ defaultTimeout = 250 * time.Millisecond
28
+ maxConfiguredTimeout = 500 * time.Millisecond
29
+ defaultStartupTimeout = 30 * time.Second
30
+ defaultConcurrency = 1
31
+
32
+ // Packing remains token-exact for admitted inputs, but hook-controlled
33
+ // strings must be bounded before the tokenizer sees them. Each limit is
34
+ // still orders of magnitude above its final token budget.
35
+ maxPretokenizedFieldBytes = 64 * 1024
36
+ maxPretokenizedTotalBytes = 128 * 1024
37
+ )
38
+
39
+ const (
40
+ DecisionSafe = "safe"
41
+ DecisionUnsafe = "unsafe"
42
+ DecisionUnavailable = "unavailable"
43
+ )
44
+
45
+ const (
46
+ ErrorTimeout = "timeout"
47
+ ErrorUnavailable = "unavailable"
48
+ ErrorExcludedTool = "excluded_tool"
49
+ ErrorActionTooLarge = "action_too_large"
50
+ ErrorRequestTooLarge = "request_too_large"
51
+ ErrorSchemaTooLarge = "schema_too_large"
52
+ ErrorUnsupportedText = "unsupported_text"
53
+ ErrorInvalidHistory = "invalid_history"
54
+ ErrorInference = "inference_error"
55
+ ErrorConcurrency = "concurrency_timeout"
56
+ ErrorInvalidOutput = "invalid_output"
57
+ ErrorInputTooLarge = "input_too_large"
58
+ )
59
+
60
+ // Input is the complete production-available no-Thought representation. The
61
+ // action stays structured until Go renders the training form.
62
+ type Input struct {
63
+ UserRequest string `json:"user_request"`
64
+ RequestTooLarge bool `json:"request_too_large"`
65
+ InteractionHistory string `json:"interaction_history"`
66
+ HistoryOmitted bool `json:"history_omitted"`
67
+ ToolName string `json:"tool_name"`
68
+ ToolArguments any `json:"tool_arguments"`
69
+ AvailableToolSchemas any `json:"available_tool_schemas"`
70
+ }
71
+
72
+ // Evaluation is returned for every enabled PreToolUse call. UnsafeProbability
73
+ // retains its wire name for compatibility; V3 supplies an agreement score, not
74
+ // a calibrated risk probability. It is absent on abstention or failure; ShadowDecision then becomes unavailable and the real
75
+ // tool authorization remains untouched (fail open for this pilot).
76
+ type Evaluation struct {
77
+ UnsafeProbability *float64 `json:"unsafe_probability,omitempty"`
78
+ ShadowDecision string `json:"shadow_decision"`
79
+ Threshold float64 `json:"threshold"`
80
+ ModelVersion string `json:"model_version"`
81
+ LatencyMS float64 `json:"latency_ms"`
82
+ ErrorCode string `json:"error_code,omitempty"`
83
+ Enforced bool `json:"enforced"`
84
+ UserRequestPresent bool `json:"user_request_present"`
85
+ HistoryPresent bool `json:"history_present"`
86
+ HistoryOmitted bool `json:"history_omitted"`
87
+ ToolSchemasPresent bool `json:"tool_schemas_present"`
88
+ }
89
+
90
+ type Health struct {
91
+ Enabled bool `json:"enabled"`
92
+ Status string `json:"status"`
93
+ ModelVersion string `json:"model_version,omitempty"`
94
+ Device string `json:"device,omitempty"`
95
+ ErrorCode string `json:"error_code,omitempty"`
96
+ }
97
+
98
+ type Config struct {
99
+ Enabled bool
100
+ // ModelDir is retained for compatibility with historical development callers.
101
+ // Production inference always uses the embedded checkpoint.
102
+ ModelDir string
103
+ Timeout time.Duration
104
+ StartupTimeout time.Duration
105
+ MaxConcurrency int
106
+ ModelVersion string
107
+ }
108
+
109
+ // Backend is the narrow boundary between safety policy plumbing and local
110
+ // inference. Production uses the embedded Go model; tests can use deterministic fakes.
111
+ type Backend interface {
112
+ Infer(context.Context, Input) (InferenceResult, error)
113
+ Health(context.Context) (Health, error)
114
+ Close() error
115
+ }
116
+
117
+ type InferenceResult struct {
118
+ Logits [2]float64
119
+ HistoryOmitted bool
120
+ }
121
+
122
+ type codedError struct {
123
+ code string
124
+ err error
125
+ }
126
+
127
+ func (e codedError) Error() string { return e.err.Error() }
128
+ func (e codedError) Unwrap() error { return e.err }
129
+
130
+ func backendError(code string, err error) error {
131
+ if err == nil {
132
+ err = errors.New(code)
133
+ }
134
+ return codedError{code: code, err: err}
135
+ }
136
+
137
+ type Evaluator struct {
138
+ backend Backend
139
+ timeout time.Duration
140
+ slots chan struct{}
141
+ modelVersion string
142
+ unavailable string
143
+ }
144
+
145
+ func ConfigFromEnv(_ string) (Config, error) {
146
+ // Advisory assessment is enabled unless the daemon explicitly opts out.
147
+ enabled, err := envBool("KONTEXT_STEP_SAFETY_SHADOW", true)
148
+ if err != nil {
149
+ return Config{}, err
150
+ }
151
+ if !enabled {
152
+ return Config{Enabled: false}, nil
153
+ }
154
+ timeout, err := envDuration("KONTEXT_STEP_SAFETY_TIMEOUT", defaultTimeout)
155
+ if err != nil {
156
+ return Config{}, err
157
+ }
158
+ if timeout > maxConfiguredTimeout {
159
+ return Config{}, fmt.Errorf("KONTEXT_STEP_SAFETY_TIMEOUT must not exceed %s", maxConfiguredTimeout)
160
+ }
161
+ startupTimeout, err := envDuration("KONTEXT_STEP_SAFETY_STARTUP_TIMEOUT", defaultStartupTimeout)
162
+ if err != nil {
163
+ return Config{}, err
164
+ }
165
+ concurrency, err := envInt("KONTEXT_STEP_SAFETY_MAX_CONCURRENCY", defaultConcurrency)
166
+ if err != nil {
167
+ return Config{}, err
168
+ }
169
+ if concurrency != 1 {
170
+ return Config{}, errors.New("KONTEXT_STEP_SAFETY_MAX_CONCURRENCY must be 1 for the singleton pilot session")
171
+ }
172
+ // One evaluation runs at a time. Admission stays within the same deadline.
173
+ return Config{
174
+ Enabled: enabled,
175
+ Timeout: timeout,
176
+ StartupTimeout: startupTimeout,
177
+ MaxConcurrency: concurrency,
178
+ ModelVersion: ModelVersion,
179
+ }, nil
180
+ }
181
+
182
+ // New loads the embedded native Go checkpoint when enabled. Initialization
183
+ // failures produce an unavailable evaluator rather than failing daemon startup.
184
+ func New(ctx context.Context, cfg Config) *Evaluator {
185
+ if !cfg.Enabled {
186
+ return nil
187
+ }
188
+ cfg = normalizeConfig(cfg)
189
+ backend, err := newNativeBackend(ctx, cfg)
190
+ if err != nil {
191
+ return &Evaluator{
192
+ timeout: cfg.Timeout,
193
+ slots: make(chan struct{}, cfg.MaxConcurrency),
194
+ modelVersion: cfg.ModelVersion,
195
+ unavailable: errorCode(err),
196
+ }
197
+ }
198
+ return NewWithBackend(backend, cfg.Timeout, cfg.MaxConcurrency, cfg.ModelVersion)
199
+ }
200
+
201
+ func NewWithBackend(backend Backend, timeout time.Duration, maxConcurrency int, modelVersion string) *Evaluator {
202
+ if timeout <= 0 {
203
+ timeout = defaultTimeout
204
+ }
205
+ if maxConcurrency <= 0 {
206
+ maxConcurrency = defaultConcurrency
207
+ }
208
+ if strings.TrimSpace(modelVersion) == "" {
209
+ modelVersion = ModelVersion
210
+ }
211
+ return &Evaluator{
212
+ backend: backend,
213
+ timeout: timeout,
214
+ slots: make(chan struct{}, maxConcurrency),
215
+ modelVersion: modelVersion,
216
+ }
217
+ }
218
+
219
+ func normalizeConfig(cfg Config) Config {
220
+ if cfg.Timeout <= 0 {
221
+ cfg.Timeout = defaultTimeout
222
+ }
223
+ if cfg.StartupTimeout <= 0 {
224
+ cfg.StartupTimeout = defaultStartupTimeout
225
+ }
226
+ if cfg.MaxConcurrency <= 0 {
227
+ cfg.MaxConcurrency = defaultConcurrency
228
+ }
229
+ if strings.TrimSpace(cfg.ModelVersion) == "" {
230
+ cfg.ModelVersion = ModelVersion
231
+ }
232
+ return cfg
233
+ }
234
+
235
+ func (e *Evaluator) Evaluate(ctx context.Context, input Input) Evaluation {
236
+ started := time.Now()
237
+ result := Evaluation{
238
+ ShadowDecision: DecisionUnavailable,
239
+ Threshold: Threshold,
240
+ ModelVersion: ModelVersion,
241
+ Enforced: false,
242
+ UserRequestPresent: input.RequestTooLarge || strings.TrimSpace(input.UserRequest) != "",
243
+ HistoryPresent: historyIsPresent(input.InteractionHistory),
244
+ ToolSchemasPresent: input.AvailableToolSchemas != nil,
245
+ }
246
+ if e == nil {
247
+ return result
248
+ }
249
+ result.ModelVersion = e.modelVersion
250
+ result.HistoryOmitted = input.HistoryOmitted
251
+ if ExcludedTool(input.ToolName) {
252
+ result.ErrorCode = ErrorExcludedTool
253
+ result.LatencyMS = elapsedMilliseconds(started)
254
+ return result
255
+ }
256
+ if err := validateInputBounds(input); err != nil {
257
+ result.ErrorCode = errorCode(err)
258
+ result.LatencyMS = elapsedMilliseconds(started)
259
+ return result
260
+ }
261
+ if e.unavailable != "" || e.backend == nil {
262
+ result.ErrorCode = firstNonEmpty(e.unavailable, ErrorUnavailable)
263
+ result.LatencyMS = elapsedMilliseconds(started)
264
+ return result
265
+ }
266
+
267
+ callCtx, cancel := context.WithTimeout(ctx, e.timeout)
268
+ defer cancel()
269
+ select {
270
+ case e.slots <- struct{}{}:
271
+ case <-callCtx.Done():
272
+ result.ErrorCode = ErrorConcurrency
273
+ result.LatencyMS = elapsedMilliseconds(started)
274
+ return result
275
+ }
276
+
277
+ // Keep the slot until native execution has actually stopped, even when
278
+ // the hook deadline wins. This bounds outstanding work and prevents late
279
+ // results from being attributed to a later request. The native binding does
280
+ // not interrupt Run, so its completion must not extend the hook deadline.
281
+ type outcome struct {
282
+ value InferenceResult
283
+ err error
284
+ }
285
+ done := make(chan outcome, 1)
286
+ go func() {
287
+ defer func() { <-e.slots }()
288
+ value, err := e.backend.Infer(callCtx, input)
289
+ done <- outcome{value, err}
290
+ }()
291
+ var output outcome
292
+ select {
293
+ case output = <-done:
294
+ case <-callCtx.Done():
295
+ output.err = callCtx.Err()
296
+ }
297
+ result.LatencyMS = elapsedMilliseconds(started)
298
+ if callCtx.Err() != nil {
299
+ output.err = callCtx.Err()
300
+ }
301
+ if output.err != nil {
302
+ result.ErrorCode = errorCode(output.err)
303
+ return result
304
+ }
305
+ result.HistoryOmitted = result.HistoryOmitted || output.value.HistoryOmitted
306
+ logits := output.value.Logits
307
+ if math.IsNaN(logits[0]) || math.IsNaN(logits[1]) || math.IsInf(logits[0], 0) || math.IsInf(logits[1], 0) {
308
+ result.ErrorCode = ErrorInvalidOutput
309
+ return result
310
+ }
311
+ probability := CalibratedProbability(logits[1] - logits[0])
312
+ result.UnsafeProbability = &probability
313
+ result.ShadowDecision = DecisionSafe
314
+ if probability >= Threshold {
315
+ result.ShadowDecision = DecisionUnsafe
316
+ }
317
+ return result
318
+ }
319
+
320
+ func historyIsPresent(history string) bool {
321
+ normalized := strings.TrimSpace(history)
322
+ return normalized != "" && normalized != "[]"
323
+ }
324
+
325
+ func validateInputBounds(input Input) error {
326
+ if input.RequestTooLarge || len(input.UserRequest) > maxPretokenizedFieldBytes || len(input.InteractionHistory) > maxPretokenizedFieldBytes || len(input.ToolName) > 256 {
327
+ return backendError(ErrorInputTooLarge, nil)
328
+ }
329
+ for _, value := range []any{input.ToolArguments, input.AvailableToolSchemas} {
330
+ remaining := maxPretokenizedFieldBytes
331
+ if err := boundedJSONSize(value, &remaining, 0); err != nil {
332
+ return err
333
+ }
334
+ }
335
+
336
+ arguments, err := json.Marshal(input.ToolArguments)
337
+ if err != nil {
338
+ return backendError(ErrorInference, errors.New("step-safety arguments are not JSON encodable"))
339
+ }
340
+ schemas := []byte(nil)
341
+ if input.AvailableToolSchemas != nil {
342
+ schemas, err = json.Marshal(input.AvailableToolSchemas)
343
+ if err != nil {
344
+ return backendError(ErrorInference, errors.New("step-safety schemas are not JSON encodable"))
345
+ }
346
+ }
347
+ fields := []int{
348
+ len(input.UserRequest),
349
+ len(input.InteractionHistory),
350
+ len(input.ToolName) + len(arguments) + len("[TOOL_NAME]\n\n[ARGUMENTS]\n"),
351
+ len(schemas),
352
+ }
353
+ total := 0
354
+ for _, size := range fields {
355
+ if size > maxPretokenizedFieldBytes {
356
+ return backendError(ErrorInputTooLarge, errors.New("step-safety input field exceeds preprocessing bound"))
357
+ }
358
+ total += size
359
+ }
360
+ if total > maxPretokenizedTotalBytes {
361
+ return backendError(ErrorInputTooLarge, errors.New("step-safety input exceeds preprocessing bound"))
362
+ }
363
+ return nil
364
+ }
365
+
366
+ func CalibratedProbability(margin float64) float64 {
367
+ z := calibrationScale*margin + calibrationBias
368
+ if z >= 0 {
369
+ return 1 / (1 + math.Exp(-z))
370
+ }
371
+ exp := math.Exp(z)
372
+ return exp / (1 + exp)
373
+ }
374
+
375
+ func (e *Evaluator) Health(ctx context.Context) Health {
376
+ if e == nil {
377
+ return Health{Enabled: false, Status: "disabled"}
378
+ }
379
+ if e.unavailable != "" || e.backend == nil {
380
+ return Health{Enabled: true, Status: "unavailable", ModelVersion: e.modelVersion, ErrorCode: firstNonEmpty(e.unavailable, ErrorUnavailable)}
381
+ }
382
+ health, err := e.backend.Health(ctx)
383
+ if err != nil {
384
+ return Health{Enabled: true, Status: "unavailable", ModelVersion: e.modelVersion, ErrorCode: errorCode(err)}
385
+ }
386
+ health.Enabled = true
387
+ if health.ModelVersion == "" {
388
+ health.ModelVersion = e.modelVersion
389
+ }
390
+ return health
391
+ }
392
+
393
+ func (e *Evaluator) Close() error {
394
+ if e == nil || e.backend == nil {
395
+ return nil
396
+ }
397
+ return e.backend.Close()
398
+ }
399
+
400
+ func errorCode(err error) string {
401
+ if err == nil {
402
+ return ""
403
+ }
404
+ if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
405
+ return ErrorTimeout
406
+ }
407
+ var coded codedError
408
+ if errors.As(err, &coded) && coded.code != "" {
409
+ return coded.code
410
+ }
411
+ return ErrorInference
412
+ }
413
+
414
+ func elapsedMilliseconds(started time.Time) float64 {
415
+ return float64(time.Since(started).Microseconds()) / 1000
416
+ }
417
+
418
+ func firstNonEmpty(values ...string) string {
419
+ for _, value := range values {
420
+ if value != "" {
421
+ return value
422
+ }
423
+ }
424
+ return ""
425
+ }
426
+
427
+ func envString(key, fallback string) string {
428
+ if value := strings.TrimSpace(os.Getenv(key)); value != "" {
429
+ return value
430
+ }
431
+ return fallback
432
+ }
433
+
434
+ func envBool(key string, fallback bool) (bool, error) {
435
+ value := strings.TrimSpace(os.Getenv(key))
436
+ if value == "" {
437
+ return fallback, nil
438
+ }
439
+ parsed, err := strconv.ParseBool(value)
440
+ if err != nil {
441
+ return false, fmt.Errorf("%s must be a boolean: %w", key, err)
442
+ }
443
+ return parsed, nil
444
+ }
445
+
446
+ func envDuration(key string, fallback time.Duration) (time.Duration, error) {
447
+ value := strings.TrimSpace(os.Getenv(key))
448
+ if value == "" {
449
+ return fallback, nil
450
+ }
451
+ parsed, err := time.ParseDuration(value)
452
+ if err != nil || parsed <= 0 {
453
+ return 0, fmt.Errorf("%s must be a positive duration", key)
454
+ }
455
+ return parsed, nil
456
+ }
457
+
458
+ func envInt(key string, fallback int) (int, error) {
459
+ value := strings.TrimSpace(os.Getenv(key))
460
+ if value == "" {
461
+ return fallback, nil
462
+ }
463
+ parsed, err := strconv.Atoi(value)
464
+ if err != nil {
465
+ return 0, fmt.Errorf("%s must be an integer: %w", key, err)
466
+ }
467
+ return parsed, nil
468
+ }
v3-ungated/native/stepsafety/unicode9.go ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Code generated by UPDATE_GOLDEN=1 go test -run TestPostUnicode9TableCoversXText; DO NOT EDIT.
2
+
3
+ package stepsafety
4
+
5
+ import "unicode"
6
+
7
+ // postUnicode9NFC holds the runes Unicode 9.0 leaves unassigned that the
8
+ // Unicode 17.0.0 normalization tables reorder, compose or decompose.
9
+ var postUnicode9NFC = &unicode.RangeTable{
10
+ R16: []unicode.Range16{
11
+ {Lo: 0x07fd, Hi: 0x07fd, Stride: 1},
12
+ {Lo: 0x0897, Hi: 0x089f, Stride: 1},
13
+ {Lo: 0x08ca, Hi: 0x08d3, Stride: 1},
14
+ {Lo: 0x09fe, Hi: 0x09fe, Stride: 1},
15
+ {Lo: 0x0c3c, Hi: 0x0c3c, Stride: 1},
16
+ {Lo: 0x0d3b, Hi: 0x0d3c, Stride: 1},
17
+ {Lo: 0x0eba, Hi: 0x0eba, Stride: 1},
18
+ {Lo: 0x1715, Hi: 0x1715, Stride: 1},
19
+ {Lo: 0x1abf, Hi: 0x1add, Stride: 1},
20
+ {Lo: 0x1ae0, Hi: 0x1aeb, Stride: 1},
21
+ {Lo: 0x1df6, Hi: 0x1dfa, Stride: 1},
22
+ {Lo: 0xa82c, Hi: 0xa82c, Stride: 1},
23
+ },
24
+ R32: []unicode.Range32{
25
+ {Lo: 0x105c9, Hi: 0x105c9, Stride: 1},
26
+ {Lo: 0x105d2, Hi: 0x105d2, Stride: 1},
27
+ {Lo: 0x105da, Hi: 0x105da, Stride: 1},
28
+ {Lo: 0x105e4, Hi: 0x105e4, Stride: 1},
29
+ {Lo: 0x10d24, Hi: 0x10d27, Stride: 1},
30
+ {Lo: 0x10d69, Hi: 0x10d6d, Stride: 1},
31
+ {Lo: 0x10eab, Hi: 0x10eac, Stride: 1},
32
+ {Lo: 0x10efa, Hi: 0x10efb, Stride: 1},
33
+ {Lo: 0x10efd, Hi: 0x10eff, Stride: 1},
34
+ {Lo: 0x10f46, Hi: 0x10f50, Stride: 1},
35
+ {Lo: 0x10f82, Hi: 0x10f85, Stride: 1},
36
+ {Lo: 0x11070, Hi: 0x11070, Stride: 1},
37
+ {Lo: 0x1133b, Hi: 0x1133b, Stride: 1},
38
+ {Lo: 0x11382, Hi: 0x11385, Stride: 1},
39
+ {Lo: 0x1138b, Hi: 0x1138b, Stride: 1},
40
+ {Lo: 0x1138e, Hi: 0x1138e, Stride: 1},
41
+ {Lo: 0x11390, Hi: 0x11391, Stride: 1},
42
+ {Lo: 0x113b8, Hi: 0x113b8, Stride: 1},
43
+ {Lo: 0x113bb, Hi: 0x113bb, Stride: 1},
44
+ {Lo: 0x113c2, Hi: 0x113c2, Stride: 1},
45
+ {Lo: 0x113c5, Hi: 0x113c5, Stride: 1},
46
+ {Lo: 0x113c7, Hi: 0x113c9, Stride: 1},
47
+ {Lo: 0x113ce, Hi: 0x113d0, Stride: 1},
48
+ {Lo: 0x1145e, Hi: 0x1145e, Stride: 1},
49
+ {Lo: 0x11839, Hi: 0x1183a, Stride: 1},
50
+ {Lo: 0x11930, Hi: 0x11930, Stride: 1},
51
+ {Lo: 0x11935, Hi: 0x11935, Stride: 1},
52
+ {Lo: 0x11938, Hi: 0x11938, Stride: 1},
53
+ {Lo: 0x1193d, Hi: 0x1193e, Stride: 1},
54
+ {Lo: 0x11943, Hi: 0x11943, Stride: 1},
55
+ {Lo: 0x119e0, Hi: 0x119e0, Stride: 1},
56
+ {Lo: 0x11a34, Hi: 0x11a34, Stride: 1},
57
+ {Lo: 0x11a47, Hi: 0x11a47, Stride: 1},
58
+ {Lo: 0x11a99, Hi: 0x11a99, Stride: 1},
59
+ {Lo: 0x11d42, Hi: 0x11d42, Stride: 1},
60
+ {Lo: 0x11d44, Hi: 0x11d45, Stride: 1},
61
+ {Lo: 0x11d97, Hi: 0x11d97, Stride: 1},
62
+ {Lo: 0x11f41, Hi: 0x11f42, Stride: 1},
63
+ {Lo: 0x1611e, Hi: 0x16129, Stride: 1},
64
+ {Lo: 0x1612f, Hi: 0x1612f, Stride: 1},
65
+ {Lo: 0x16d63, Hi: 0x16d63, Stride: 1},
66
+ {Lo: 0x16d67, Hi: 0x16d6a, Stride: 1},
67
+ {Lo: 0x16ff0, Hi: 0x16ff1, Stride: 1},
68
+ {Lo: 0x1e08f, Hi: 0x1e08f, Stride: 1},
69
+ {Lo: 0x1e130, Hi: 0x1e136, Stride: 1},
70
+ {Lo: 0x1e2ae, Hi: 0x1e2ae, Stride: 1},
71
+ {Lo: 0x1e2ec, Hi: 0x1e2ef, Stride: 1},
72
+ {Lo: 0x1e4ec, Hi: 0x1e4ef, Stride: 1},
73
+ {Lo: 0x1e5ee, Hi: 0x1e5ef, Stride: 1},
74
+ {Lo: 0x1e6e3, Hi: 0x1e6e3, Stride: 1},
75
+ {Lo: 0x1e6e6, Hi: 0x1e6e6, Stride: 1},
76
+ {Lo: 0x1e6ee, Hi: 0x1e6ef, Stride: 1},
77
+ {Lo: 0x1e6f5, Hi: 0x1e6f5, Stride: 1},
78
+ },
79
+ }
v3-ungated/native/stepsafety/unigram.go ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package stepsafety
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "math"
7
+ "strings"
8
+ "unicode/utf8"
9
+ )
10
+
11
+ const metaspace = "▁"
12
+
13
+ // unigram is the Metaspace pre-tokenizer and Unigram model of the pinned
14
+ // tokenizer.json, matching HF tokenizers 0.22.2 (Unigram::encode_optimized)
15
+ // exactly: every vocab entry is matchable, including the literal "<0xNN>"
16
+ // and bracketed special pieces; path scores accumulate in float64; the first
17
+ // candidate to reach a position wins ties; an unknown rune scores the lowest
18
+ // piece score minus 10, and consecutive unknowns fuse into one [UNK].
19
+ //
20
+ // The vocabulary is a map whose keys share one backing string, about 8 MiB.
21
+ // A byte trie with a 256-pointer array per node held the same vocabulary in
22
+ // more than 600 MiB for the lifetime of the daemon.
23
+ type unigram struct {
24
+ ids map[string]int32
25
+ scores []float32 // the artifact's scores are exact float32 values
26
+ maxLen int // longest piece in bytes
27
+ unkID int32
28
+ unkScore float64
29
+ }
30
+
31
+ func parseUnigram(data []byte) (*unigram, error) {
32
+ var config struct {
33
+ Model struct {
34
+ Type string `json:"type"`
35
+ UnkID *int `json:"unk_id"`
36
+ ByteFallback bool `json:"byte_fallback"`
37
+ Vocab []json.RawMessage `json:"vocab"`
38
+ } `json:"model"`
39
+ }
40
+ if err := json.Unmarshal(data, &config); err != nil {
41
+ return nil, err
42
+ }
43
+ model := config.Model
44
+ if model.Type != "Unigram" || model.UnkID == nil || *model.UnkID < 0 || *model.UnkID >= len(model.Vocab) || model.ByteFallback {
45
+ return nil, errors.New("unsupported tokenizer model")
46
+ }
47
+ pieces := make([]string, len(model.Vocab))
48
+ scores := make([]float32, len(model.Vocab))
49
+ size, minScore := 0, math.Inf(1)
50
+ for i, raw := range model.Vocab {
51
+ var entry [2]json.RawMessage
52
+ var score float64
53
+ if err := json.Unmarshal(raw, &entry); err != nil {
54
+ return nil, err
55
+ }
56
+ if err := json.Unmarshal(entry[0], &pieces[i]); err != nil {
57
+ return nil, err
58
+ }
59
+ if err := json.Unmarshal(entry[1], &score); err != nil {
60
+ return nil, err
61
+ }
62
+ if float64(float32(score)) != score || pieces[i] == "" || !utf8.ValidString(pieces[i]) {
63
+ return nil, errors.New("unsupported tokenizer vocabulary entry")
64
+ }
65
+ scores[i] = float32(score)
66
+ minScore = math.Min(minScore, score)
67
+ size += len(pieces[i])
68
+ }
69
+ var backing strings.Builder
70
+ backing.Grow(size)
71
+ for _, piece := range pieces {
72
+ backing.WriteString(piece)
73
+ }
74
+ all := backing.String()
75
+ u := &unigram{ids: make(map[string]int32, len(pieces)), scores: scores, unkID: int32(*model.UnkID), unkScore: minScore - 10}
76
+ offset := 0
77
+ for i, piece := range pieces {
78
+ // HF resolves duplicate pieces to the later id.
79
+ u.ids[all[offset:offset+len(piece)]] = int32(i)
80
+ offset += len(piece)
81
+ u.maxLen = max(u.maxLen, len(piece))
82
+ }
83
+ return u, nil
84
+ }
85
+
86
+ // encode pre-tokenizes one normalized segment that holds no special tokens:
87
+ // spaces become ▁, a ▁ is prepended unless present, and every word starts at
88
+ // a ▁. Each word is segmented independently.
89
+ func (u *unigram) encode(normalized string, ids []int64) []int64 {
90
+ if normalized == "" {
91
+ return ids
92
+ }
93
+ text := strings.ReplaceAll(normalized, " ", metaspace)
94
+ if !strings.HasPrefix(text, metaspace) {
95
+ text = metaspace + text
96
+ }
97
+ for len(text) > 0 {
98
+ word := text
99
+ if next := strings.Index(text[len(metaspace):], metaspace); next >= 0 {
100
+ word = text[:len(metaspace)+next]
101
+ }
102
+ ids = u.segment(word, ids)
103
+ text = text[len(word):]
104
+ }
105
+ return ids
106
+ }
107
+
108
+ type unigramNode struct {
109
+ score float64
110
+ start int // -1 until reached
111
+ id int32
112
+ }
113
+
114
+ // segment appends the best-scoring segmentation of word.
115
+ func (u *unigram) segment(word string, ids []int64) []int64 {
116
+ n := len(word)
117
+ nodes := make([]unigramNode, n+1)
118
+ for i := range nodes {
119
+ nodes[i].start = -1
120
+ }
121
+ relax := func(end, start int, id int32, score float64) {
122
+ if node := &nodes[end]; node.start < 0 || score > node.score {
123
+ *node = unigramNode{score: score, start: start, id: id}
124
+ }
125
+ }
126
+ for start := 0; start < n; {
127
+ here := nodes[start].score
128
+ _, runeLen := utf8.DecodeRuneInString(word[start:])
129
+ single := false
130
+ for end := start + 1; end <= min(n, start+u.maxLen); end++ {
131
+ // Pieces are valid UTF-8, so they end on rune boundaries.
132
+ if end < n && !utf8.RuneStart(word[end]) {
133
+ continue
134
+ }
135
+ id, ok := u.ids[word[start:end]]
136
+ if !ok {
137
+ continue
138
+ }
139
+ relax(end, start, id, here+float64(u.scores[id]))
140
+ single = single || end-start == runeLen
141
+ }
142
+ if !single {
143
+ relax(start+runeLen, start, u.unkID, here+u.unkScore)
144
+ }
145
+ start += runeLen
146
+ }
147
+ first := len(ids)
148
+ for end := n; end > 0; {
149
+ node := nodes[end]
150
+ start, id := node.start, node.id
151
+ if id == u.unkID {
152
+ for start > 0 && nodes[start].id == u.unkID {
153
+ start = nodes[start].start
154
+ }
155
+ // HF maps the fused surface string back through the vocabulary.
156
+ if known, ok := u.ids[word[start:end]]; ok {
157
+ id = known
158
+ }
159
+ }
160
+ ids = append(ids, int64(id))
161
+ end = start
162
+ }
163
+ for i, j := first, len(ids)-1; i < j; i, j = i+1, j-1 {
164
+ ids[i], ids[j] = ids[j], ids[i]
165
+ }
166
+ return ids
167
+ }
v3-ungated/release_manifest.json ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "contains_private_examples": false,
3
+ "files": {
4
+ "LICENSE": {
5
+ "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
6
+ "size": 11357
7
+ },
8
+ "NOTICE": {
9
+ "sha256": "5f307f4fc93430c9abe22bec00c00f3844291724bef614ac0a527a163ca5eab1",
10
+ "size": 271
11
+ },
12
+ "README.md": {
13
+ "sha256": "5d4285e2dd08af2d9a449889780ab2cf55c8ba13c1d8154aae04c734b2b04128",
14
+ "size": 4424
15
+ },
16
+ "evaluation.json": {
17
+ "sha256": "186ec26dd500b5f8791283a619200450c0f10e523ce3faca92a296f6acec1562",
18
+ "size": 1517
19
+ },
20
+ "native/cmd/merlin/main.go": {
21
+ "sha256": "0a9bbc83520ef46b1b3fc031f16fb2315f7a94d73e8dfef0d78374e64e466376",
22
+ "size": 1414
23
+ },
24
+ "native/go.mod": {
25
+ "sha256": "97ffbb6542f80e9cbc14bf46354388a7ba0ce18a1ad4822b64c750b7eda7abe4",
26
+ "size": 129
27
+ },
28
+ "native/go.sum": {
29
+ "sha256": "a121c8ef8f666600e1b0dde83144361a6a67d2850f36515c00d2860adfb53b23",
30
+ "size": 312
31
+ },
32
+ "native/stepsafety/agreement.go": {
33
+ "sha256": "a19ce6cba08771b455741ba7d2622334636c50b7ffde44fc98341c767f1d2363",
34
+ "size": 3798
35
+ },
36
+ "native/stepsafety/context.go": {
37
+ "sha256": "616f619ec4ff787d810085e1566836d13e5106ec6ee42a086411999329dd1563",
38
+ "size": 7264
39
+ },
40
+ "native/stepsafety/model/ATTRIBUTION.md": {
41
+ "sha256": "35596b6f09742aa91e8e11fea77170605db0e5d62f2c818f5c90a7b6d16b520e",
42
+ "size": 616
43
+ },
44
+ "native/stepsafety/model/LICENSE": {
45
+ "sha256": "646f8936b8ddcd14e13e578ff6857e368780b0d1a4f6066bee89211923a373e2",
46
+ "size": 1073
47
+ },
48
+ "native/stepsafety/model/PROVENANCE.json": {
49
+ "sha256": "dec7f4875979af4bab488e1aca9c6fcad97b8d56ea361a40dd1da8588ae1a3ab",
50
+ "size": 3748
51
+ },
52
+ "native/stepsafety/model/native/PROVENANCE.json": {
53
+ "sha256": "2432e56272cca72e32504a0c3605c2611f957ff58e70d23095bbbad7f3844739",
54
+ "size": 2622
55
+ },
56
+ "native/stepsafety/model/native/agreement.json.gz": {
57
+ "sha256": "6ffbd35e56a031df9f2ca08f7520486cc8ef65592c8d03cfe966fbd42523bbdf",
58
+ "size": 2290715
59
+ },
60
+ "native/stepsafety/model/native/candidate.json": {
61
+ "sha256": "776a05f6f1b3854527919a24a98815d17df8a7cd2928f0d05b4cfe286217c119",
62
+ "size": 3675
63
+ },
64
+ "native/stepsafety/model/native/manifest.json": {
65
+ "sha256": "ec4ca2647df5326ac2fc919e3eb71ad82fc5597e0e077db759b430ba941e5ee5",
66
+ "size": 1591
67
+ },
68
+ "native/stepsafety/model/native/request_gate.json.gz": {
69
+ "sha256": "acfb7025661b5a424d482aae1fd351f31f714bf9c22b7c18dcabb623ca2e72c3",
70
+ "size": 2492102
71
+ },
72
+ "native/stepsafety/model/native/tokenizer.json.gz": {
73
+ "sha256": "956061a0f96d88a118848b4f32e4e5fcdc8c363f4cbc223fdc175232668aa41c",
74
+ "size": 1736709
75
+ },
76
+ "native/stepsafety/model/native/weights.00.gz": {
77
+ "sha256": "2c69b3cccc79582da64dd47ab44835604b5b4f5c676b1102eacef17dc52cacad",
78
+ "size": 46014808
79
+ },
80
+ "native/stepsafety/model/native/weights.01.gz": {
81
+ "sha256": "129de5fde020ece20ce7cf2529564c779e486e29b0c4725f7ed09af086f6db04",
82
+ "size": 41825526
83
+ },
84
+ "native/stepsafety/model/native/weights.02.gz": {
85
+ "sha256": "7cb825c60a553c37d70ee6859d597a49b0afa0e34a2a243d62903da5d297f47e",
86
+ "size": 42596160
87
+ },
88
+ "native/stepsafety/model/native/weights.03.gz": {
89
+ "sha256": "d5a9ce4740491f638c497b8e88d6d4236a1f26b3debfa9f7566a22c960828b6c",
90
+ "size": 62319867
91
+ },
92
+ "native/stepsafety/model/native/weights.04.gz": {
93
+ "sha256": "9316ce90d589cba6e3ce02f5e643fa244ff7e7bb2b5117b8508f176f01ed3000",
94
+ "size": 13716366
95
+ },
96
+ "native/stepsafety/modelstore.go": {
97
+ "sha256": "d1dfee52f8984b8aafae9f6e519bd28e5cefc2dce274fee46cd3e4ba18b0fd92",
98
+ "size": 12453
99
+ },
100
+ "native/stepsafety/native_assets.go": {
101
+ "sha256": "73781cc21e22742983297f3e216ec4ede722e9c2281eaa8bfaee4515eb02a72b",
102
+ "size": 403
103
+ },
104
+ "native/stepsafety/native_assets_nomodel.go": {
105
+ "sha256": "d1c5d28591ae19d1f9003ffb29f56a7e2a0491c03a16ac2ab6c6400ec32e4dc6",
106
+ "size": 454
107
+ },
108
+ "native/stepsafety/native_backend.go": {
109
+ "sha256": "e94731e7b57cd0bdac1b045dd76f10fcf8690a5bec67ef2b5f1b97d7141f97b1",
110
+ "size": 3611
111
+ },
112
+ "native/stepsafety/native_linear_arm64.go": {
113
+ "sha256": "2a510eb9fc849bdf59d4e13fc9288dc14bca41034fb307f76f84ab30d49cb53b",
114
+ "size": 2108
115
+ },
116
+ "native/stepsafety/native_linear_arm64.s": {
117
+ "sha256": "f7d41e764cb85dc8b855d6a7f94834681c9aff236e67f3d1b04d2efa138de4b7",
118
+ "size": 1340
119
+ },
120
+ "native/stepsafety/native_linear_generic.go": {
121
+ "sha256": "c54f032dcb5a9b574cd539b838eacd83b71376b898edbb771e06ee5a191d947b",
122
+ "size": 690
123
+ },
124
+ "native/stepsafety/native_model.go": {
125
+ "sha256": "02fc641a5439fd359bd5f8dd5d6f26f4396ee2bbf0f3b06b6dac2c029989addc",
126
+ "size": 9618
127
+ },
128
+ "native/stepsafety/native_weights.go": {
129
+ "sha256": "104a1ea3851d97d920e1ae7b60d2b21be3ad58470a089fc5538eb2ba511af511",
130
+ "size": 4065
131
+ },
132
+ "native/stepsafety/native_workspace.go": {
133
+ "sha256": "6aa87fec23a4a88783990463dca83015512c93d9fcca1829e559cab729b64211",
134
+ "size": 1420
135
+ },
136
+ "native/stepsafety/normalization.go": {
137
+ "sha256": "b67a2422a587b6d59719dfb4027058b6f32495dec29ddb75d8524d1c0e1ba434",
138
+ "size": 5107
139
+ },
140
+ "native/stepsafety/packing.go": {
141
+ "sha256": "694c59c3d9e4724b912d34a7101d07fcf6fb5d2449883216eba11379e1c4f9d5",
142
+ "size": 5942
143
+ },
144
+ "native/stepsafety/request_gate.go": {
145
+ "sha256": "cb94dd7a32ab72e7a3f75017d297ee74a17ca0baa56283194f6f8febac2b7d83",
146
+ "size": 2758
147
+ },
148
+ "native/stepsafety/runtime_artifacts.go": {
149
+ "sha256": "cee5b858b63b60227d32ac241a47e7428df5f9d98a66c3397cf64f1fa035a3f4",
150
+ "size": 1672
151
+ },
152
+ "native/stepsafety/runtime_store.go": {
153
+ "sha256": "cb4fc9d15c372d303a76bd878ef2a992bb72eea728636792169c1f727ea5b25f",
154
+ "size": 5702
155
+ },
156
+ "native/stepsafety/scoped.go": {
157
+ "sha256": "00ffb738e773127099f9b25bcd4e7772e7b6796af17ef3ae76cb63960d1e2927",
158
+ "size": 5510
159
+ },
160
+ "native/stepsafety/tokenizer.go": {
161
+ "sha256": "3530fd17e4d5898636f19ff9246e6305b7e322373905d1bd83508042274f5b5c",
162
+ "size": 3663
163
+ },
164
+ "native/stepsafety/types.go": {
165
+ "sha256": "faa85a9c0438c6bffed0184d18d1531481d42500d0b7c9679f77d1a6a110c54a",
166
+ "size": 13934
167
+ },
168
+ "native/stepsafety/unicode9.go": {
169
+ "sha256": "3b110e09e6901a4d79cb085bbfaaeda4186e03c75a3ce196de29a3b7ee669c3d",
170
+ "size": 3024
171
+ },
172
+ "native/stepsafety/unigram.go": {
173
+ "sha256": "0a79d9c5c75306be59647997d8d29d2d2a917455f72a7fdf1d8a40bc62a09df0",
174
+ "size": 5041
175
+ }
176
+ },
177
+ "runtime": "merlin-v3-ungated-20261104",
178
+ "schema_version": 1,
179
+ "weights_candidate": "merlin-v3-precision-20261007"
180
+ }