Skip to content

policy: hold model-weight writes and hub publishes (TK-A056..A058) - #649

Open
coughlmao wants to merge 3 commits into
Cygnux-Labs:mainfrom
coughlmao:fix/model-weight-write-policy
Open

coughlmao wants to merge 3 commits into
Cygnux-Labs:mainfrom
coughlmao:fix/model-weight-write-policy

Conversation

@coughlmao

Copy link
Copy Markdown

Summary

Add three ask rules to tracekit/policy2/packs/coding.yaml:

  • TK-A056 (class: fs): Write/Edit/MultiEdit/NotebookEdit to *.safetensors, *.gguf, *.pt, *.pth, or *.ckpt paths.
  • TK-A057 (class: shell, field: effects): shell write targets ending in the same extensions (cp, mv, tee, dd, sed -i, redirections, …), as TK-D008 reads them.
  • TK-A058 (class: shell, field: argv): huggingface-cli upload, hf upload, ollama push (up to eight options before the subcommand); downloads (download, pull) are not held.

All three fire in coding.yaml, dev.yaml, server.yaml and strict.yaml.

Also:

  • Bump pack version to 2026.10-v2-64.
  • Add corpus cases to tests/data/policy/deny/shell.jsonl, deny/fs.jsonl, benign/shell.jsonl and benign/fs.jsonl.
  • Add test_writing_or_publishing_model_weights_asks_in_every_pack in tests/test_signer_policy.py covering all four packs and both engines.
  • Update docs/policy-v2.md coding pack ask table with TK-A056..A058.
  • Add docs/limits.md row for what the rules don't cover (torch.save inside a Python script, unlisted formats .bin/.h5/.onnx/.pkl, Python SDK uploads).
  • Re-record eval/results/e17_policy_corpus.json and e21_policy_robustness.json.

E17: 100% deny recall, 0.11% benign FP, no engine mismatches. E21 latency: p99 <= 853 us on the fallback engine (new patterns under 7 ms on 64 KiB each, well within the 50 ms budget).
tracekit policy lint: ok on all packs.

Closes #560

What and why

Overwriting a model's weights, or publishing them to a model hub, changes what every user of that model gets, and nothing held it before this change. Motivated by a recent MIT CSAIL result showing a model editing another model's weights from a text prompt, changing 0.016% of them — a change invisible in the file and hard to catch by evaluation, so the write or publish moment is the right place to hold.

Three ask rules added to tracekit/policy2/packs/coding.yaml (and inherited by dev.yaml, server.yaml and strict.yaml):

  • TK-A056 (class: fs, field: path): holds Write, Edit, MultiEdit and NotebookEdit to *.safetensors, *.gguf, *.pt, *.pth or *.ckpt paths. Reads are not held.
  • TK-A057 (class: shell, field: effects): holds shell commands whose write targets (as TK-D008 reads them: redirections, cp, mv, tee, dd, sed -i, …) end in those extensions. Reads, listings and writes to other extensions are not held.
  • TK-A058 (class: shell, field: argv): holds huggingface-cli upload, hf upload and ollama push, after up to eight options. Downloads (download, pull) are not held.

How I ran it as a user

Ran tracekit policy lint on every pack in tracekit/policy2/packs/:

ok: tracekit/policy2/packs/browser.yaml 
ok: tracekit/policy2/packs/coding.yaml 
ok: tracekit/policy2/packs/dev.yaml 
ok: tracekit/policy2/packs/server-cloud.yaml 
ok: tracekit/policy2/packs/server-comms.yaml 
ok: tracekit/policy2/packs/server-data.yaml 
ok: tracekit/policy2/packs/server-net.yaml 
ok: tracekit/policy2/packs/server.yaml 
ok: tracekit/policy2/packs/strict.yaml

Ran the E17 corpus against server.yaml (which composes coding, browser and
server-* packs), both engines:

Class Deny recall Benign FP
shell 346/346 0/271
fs 37/37 0/63
Total 100.0%, 0.11%
  • Engine mismatches (RE2 vs regex): 0
  • Unmapped tools: 0

The five commands from the issue (huggingface-cli upload, hf upload, ollama push, cp new.safetensors …, and Write to .safetensors) each came back ask with the matching rule ID.

The benign lookalikes (download, pull, head -c 100 model.safetensors, and a commit message naming the file) each came back allow with no rule hit on TK-A056–TK-A058.

Regression coverage

Added test_writing_or_publishing_model_weights_asks_in_every_pack to tests/test_signer_policy.py.

The test covers all four packs (coding, dev, server, strict) and both engines (RE2 and regex), with 248 subtests covering held file-tool operations, shell writes, model-hub publishing, and benign cases.

Added corpus cases to:

  • tests/data/policy/deny/shell.jsonl
  • tests/data/policy/deny/fs.jsonl
  • tests/data/policy/benign/shell.jsonl
  • tests/data/policy/benign/fs.jsonl

E21 robustness evaluation

Re-recorded eval/results/e21_policy_robustness.json.

  • Fallback-engine p99 latency: ≤853 µs.
  • New patterns each complete in under 7 ms on 64 KiB inputs.
  • All patterns remain within the 50 ms execution budget.

Documentation

  • Updated docs/policy-v2.md with TK-A056–TK-A058 in the coding pack ask table.
  • Updated docs/limits.md with the boundaries of these rules.

The rules do not cover torch.save() inside a Python script, unlisted formats such as .bin, .h5, .onnx, .pkl and .npz, or Python SDK uploads such as huggingface_hub.upload_folder(...).

Checklist

  • make check passes (lint, tests, build); make test-ts too if sdk/typescript/ changed
  • a regression test covers the change
  • user-visible changes are described above (release notes are written at release time; don't edit CHANGELOG.md)
  • docs updated if behaviour or a security claim changed (docs/threat-model-laptop.md,
    docs/threat-model-server.md)
  • every commit is signed off (git commit -s, see CONTRIBUTING.md)

Security invariants

  • the agent still never holds a signing key or assigns sequence numbers
  • nothing an agent-controlled process supplies (isolation level, fail mode, gap/tamper records,
    approval identity) is newly trusted by the signer or verifier
  • gaps are still signed, never silent; a check that cannot run warns instead of passing
  • no verification code is shipped inside a bundle

Evidence format

  • this change does not touch the frozen formats: v1 (core.sig_message, ledger.make_record, the v1 schema, how
    the verifier treats existing bundles) or the published v2 format
  • or, if it does: the issue explicitly asks for it, existing bundles (including docs/sample/ and
    tests/fixtures/) still verify, and the change is called out above

Add three ask rules to tracekit/policy2/packs/coding.yaml:

- TK-A056 (class: fs): Write/Edit/MultiEdit/NotebookEdit to *.safetensors,
  *.gguf, *.pt, *.pth, or *.ckpt paths.
- TK-A057 (class: shell, field: effects): shell write targets ending in the
  same extensions (cp, mv, tee, dd, sed -i, redirections, …), as TK-D008
  reads them.
- TK-A058 (class: shell, field: argv): huggingface-cli upload, hf upload,
  ollama push (up to eight options before the subcommand); downloads
  (download, pull) are not held.

All three fire in coding.yaml, dev.yaml, server.yaml and strict.yaml.

Also:
- Bump pack version to 2026.10-v2-64.
- Add corpus cases to tests/data/policy/deny/shell.jsonl, deny/fs.jsonl,
  benign/shell.jsonl and benign/fs.jsonl.
- Add test_writing_or_publishing_model_weights_asks_in_every_pack in
  tests/test_signer_policy.py covering all four packs and both engines.
- Update docs/policy-v2.md coding pack ask table with TK-A056..A058.
- Add docs/limits.md row for what the rules don't cover (torch.save inside
  a Python script, unlisted formats .bin/.h5/.onnx/.pkl, Python SDK uploads).
- Re-record eval/results/e17_policy_corpus.json and e21_policy_robustness.json.

E17: 100% deny recall, 0.11% benign FP, no engine mismatches.
E21 latency: p99 <= 853 us on the fallback engine (new patterns under 7 ms
on 64 KiB each, well within the 50 ms budget).
tracekit policy lint: ok on all packs.

Closes Cygnux-Labs#560

Signed-off-by: coughlmao <coughlmao@users.noreply.github.com>
Signed-off-by: Mohit Anand <anandm.mohit@gmail.com>
@LoopGlitch26

Copy link
Copy Markdown
Member

Thanks. I loaded this pack into main's engine and compared its verdicts with main's on the policy corpus, E21's pools and the strings in the policy tests (11,760 calls, every pack, both engines): nothing main stops runs now, no benign corpus call changes, the engines agree, and each new pattern takes at most 5 ms on 64 KiB on the fallback engine (0.1 ms on RE2). These need fixing before it can merge:

  1. Rule ids. TK-A056 and TK-A057 are also used by Policy: the coding default holds secrets, remote deletes, access changes, guardrail changes and pushes to main #605, which is open. Please use TK-A058, TK-A059 and TK-A060 (pack, test, docs and limits row).
  2. TK-A058 misses upload-large-folder, the CLI's other publish command (hf upload-large-folder acme/model ./out, and the same with huggingface-cli). Use upload(?:-large-folder)? and add both to the test.
  3. TK-A057 stops at a newline in a path: cp x 'a\nb.safetensors' isn't held. Read the target as TK-D008 does: '^WRITE_TARGET:[\w\W]*?\.(?:safetensors|gguf|pt|pth|ckpt)$'. Keep it lazy: 1.3 ms on 64 KiB on the fallback engine, where a greedy [\w\W]* takes 208 ms. Add that command to the test.
  4. Reason and remedy. TK-A056 and TK-A057 hold every write to a weights file, a new one too (python train.py > out.pt), so "overwriting" isn't what they read, and saving under a new name is held as well. Suggest reason: writing a model weights file and remedy: Have a person approve the held call.
  5. E21 was re-recorded. eval/results/e21_policy_robustness.json has new gates (your machine, min_heldout_recall 0.6 → 0.605, the E17 and latency caps). Put main's file back and run python eval/e21_policy_robustness.py without --record, keeping main's latency rows, which docs/evaluation.md quotes: only the policy hashes and E17's benign counts should change.
  6. eval/results/e21_robustness.json wasn't re-run. python eval/e21_robustness.py (no --rebaseline) passes and adds the 5 new harmful calls (833 originals, 2,425 rewrites) and TK-A057's and TK-A058's rows; commit it, or CI's evals job gets a different result.
  7. The docs quote the old counts, so tests/test_eval_docs.py fails: README.md (828 → 833 and 881 → 887, in three places) and docs/evaluation.md (all 833 harmful calls, 1 in 887, strict's 46 of 887 (5.2%), each of the 504 shell commands, all 833 originals and all 2,425 rewrites).
  8. Limits row. Also name what it doesn't read: a folder copied or unpacked (cp -r out/ models/, rsync, tar -x, unzip), a download's own output option (curl -o, wget -O; a redirect is held), deleting weights (rm), and a hub repository pushed with Git (git push or git lfs push to huggingface.co). For "What to do": keep production weights where the agent can't write (a read-only mount), and publish from CI with a token the agent doesn't hold.
  9. Tests. Use self.every_pack() like the tests around it, and add an upper-case extension (echo x > MODEL.SAFETENSORS) and a redirect (python train.py > out.pt) beside the cases above.

The pack version can stay 2026.10-v2-64: other policy PRs take the same number, and we'll sort that out at merge.

@coughlmao

Copy link
Copy Markdown
Author

Hi @LoopGlitch26, I've done what you've asked for.

Please take a look at it.

Thanks!

- Updated policy hash in e17_policy_corpus.json and e21_policy_robustness.json for consistency.
- Modified rule IDs in tests/test_signer_policy.py to reflect new identifiers for model weights operations.
- Adjusted counts in e21_robustness.json to reflect changes in contained and total cases.
- Updated coding.yaml to change rule IDs and patterns related to writing and publishing model weights.

Signed-off-by: Mohit Anand <anandm.mohit@gmail.com>
@coughlmao
coughlmao force-pushed the fix/model-weight-write-policy branch from abaea2b to 8b7e115 Compare November 6, 2026 16:08
…e test that guards the fix

e21_policy_robustness.json carries main's gates and latency rows again, re-run without --record: only the policy
hashes and E17's benign counts change, and tests/test_eval_docs.py passes. The policy page names TK-A058 to TK-A060
in the coding pack's ask row (it still said TK-A056 to TK-A058) instead of a second table. The newline test writes to
the path with the newline, which the old [^\n]*? pattern didn't hold.

Signed-off-by: LoopGlitch26 <53336715+LoopGlitch26@users.noreply.github.com>
@LoopGlitch26

Copy link
Copy Markdown
Member

Thanks, almost everything from the review is in. I pushed one commit on top (8c999d9d2f) with the three small things left:

  • eval/results/e21_policy_robustness.json: main's file, re-run without --record, so its gates and latency rows stay main's. That's why test_eval_docs failed five checks.
  • The newline test now uses cp x 'a\nb.safetensors', which the old [^\n]*? pattern misses. The earlier case was held either way.
  • docs/policy-v2.md: the coding pack's row names TK-A058 to TK-A060 with their meanings, and the extra table is gone.

Nothing that main holds or denies is allowed now, and each new pattern takes 3 ms or less on 64 KiB. Once CI is green, it goes to the final check and merge.

@coughlmao

Copy link
Copy Markdown
Author

Hi @LoopGlitch26, thanks for the fixes.

I've checked the CI logs, and I've found a consistent pattern across the failing jobs.

The failures are concentrated in tests/test_model_receipt.py.

Observations:

  • Ubuntu and macOS: 16 tests fail across Python 3.9, 3.12, and 3.13.
  • Windows: 6 tests fail across Python 3.9 and 3.13.
  • Most failures report AssertionError: 2 != 0.
  • One Windows-specific test also fails with StopIteration.
  • The remaining thousands of tests are passing.

Since the same tests fail across different Python versions and operating systems, this seems more likely to be a shared issue with the model receipt functionality or its test environment than a Python-version-specific issue.

Could you please take a look? I'm happy to investigate further if needed.

@LoopGlitch26

Copy link
Copy Markdown
Member

Thanks for digging in. It comes from #570 (model receipts): those tests write .safetensors and .gguf files through the hook, and your new rules now hold those writes, so the hook exits 2. The tests need to run those writes under a policy without the weight-write rules, or approve them. I will push the test change on top of yours.

@coughlmao

Copy link
Copy Markdown
Author

Thanks for identifying the issue and for looking into it! That makes sense. Appreciate your help!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Policy: writing or publishing model weights waits for a person

2 participants