Repository navigation
Add ShyPie Privacy Gate (gateway middleware) - #685
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The summaries overstate rejection guarantees because bounded scanning can leave encoded blobs undetected, and incomplete scans are logged without rejecting the request.
1 open issue
What changed in this PR
Adds ShyPie Privacy Gate to the plugin registry as gateway middleware for structural checks on outbound content.
Changes:
- Registers
@shypie_privacy/magpie-privacy-gatewithkind: "middleware"and no provider declarations. - Adds English and Chinese summaries, while leaving the external package unmarked as community-maintained.
| File | Description |
|---|---|
registry.json |
Adds the ShyPie Privacy Gate middleware entry and bilingual summaries. |
Review effort: Balanced
| "en": "Structural egress firewall: turns away image, file and audio parts, encoded blobs and models outside the allowlist whenever magpie cannot mask the content locally, and fails closed on anything it cannot inspect.", | ||
| "zh": "结构化出口防火墙:凡是 magpie 无法在本地掩码的图片、文件与音频内容、编码数据块,以及不在允许列表中的模型,一律拒绝;无法检查的内容按失败关闭处理。" |
There was a problem hiding this comment.
Updated in v0.1.1: both summaries now describe a bounded scan — the gate does not claim to inspect every byte of an oversized field, and the README states the same plus the long-encoded-token false positive. The registry entry summary now says "the scan is bounded, so long encoded tokens can be false positives."
|
Thanks for this. I ran the published 0.1.0 package through magpie's own gateway middleware host (moejs), and ran Blocker: it rejects vendor signatures that are sent back on every later turn.
magpie's own masker leaves these values untouched (
At the default threshold, an ordinary long token in a tool result (a 450-char hash, a minified line) also gets a 403. Please say so in the README, or raise the default. Smaller things:
Not tried: the streaming paths against a real upstream (the gate has no Happy to re-review once the signature case is fixed. |
|
Thanks — all points addressed in v0.1.1 (branch updated; code at tag v0.1.1, tarball staged for npm pending 2FA). Blocker — vendor signatures: fixed with context, not key names. Exemptions apply only inside vendor blocks: Anthropic Long token false positive: documented explicitly in the README and the card summary ("the scan is bounded; a long hash or minified line can be blocked") with the recommendation to raise Smaller things: Source location: keeping it external for now — it ships as part of ShyPie and shares that repo's harness — but the exact middleware, tests and README are pinned at tag Verification: 58 harness checks + full suite; live Magpie middleware + DSH headless with DeepSeek (two turns through the gateway); temporary model allowlist blocked a DSH turn (403 from the gate) and the next turn passed after restoring; live Responses replay with |
|
Verified the signature cases against live Copilot-routed models through Magpie (middleware 0.1.1 loaded from the fixed folder):
No code change needed from this verification; 0.1.1 holds for the OpenAI and Anthropic cases. |
|
Thanks, the fix you describe (exempting signatures only inside vendor blocks) is the right shape. npm still has only 0.1.0, so I'll re-review once |
81f1960 to
431cec5
Compare
|
Decided: in-tree. The branch now adds Checks run locally on the branch:
The external Signature cases are in |
|
Thanks for moving it in-tree. I read every line at 431cec5 and ran Blocker: the exemption applies to any object that looks like a vendor block, wherever it sits.
The same values as plain Please:
Smaller
I'll re-review once those cases are in. |
|
All six bypasses now return 403. Commit The exemption is now assigned by protocol position, not by shape:
Added the six cases from your comment (all Validation on the branch: |
f061359 to
9db4a85
Compare
|
Rebased onto current The six bypass cases from the review are all in |
yetone
left a comment
There was a problem hiding this comment.
Thanks for the rework. I read every line at 9db4a85 and checked the rebase: packages/privacy-gate/ is byte-identical to f061359, and the registry entry is the same (the other registry lines are main's). On mote, bun test packages/privacy-gate passes (33/33) and bun scripts/check.mjs exits 0.
I also loaded the middleware in magpie's own host (moejs, through internal/middleware's Begin/Request, default options, model deepseek/x) and ran 40 cases. These now return 403: the six earlier bypasses, plus signature/id/encrypted_content/thoughtSignature placed in anthropic tool_use.input, tool_result.content, user and system parts, chat tool_calls[].id and arguments, chat tool/system/user parts, Responses function_call.arguments, function_call_output.output and id, user message and part id, reasoning summary, instructions, Gemini functionCall.args, functionResponse.response and systemInstruction. stream: true bodies are gated the same way. Unknown protocols and non-object bodies are refused. The anthropic thinking signature, Responses encrypted_content and model-role Gemini thoughtSignature pass. So scoping by position works for anthropic, chat and responses. Not merging yet, because these gaps remain:
1. Gemini: the exemption ignores the role. sealedFields for SITE_GEMINI_PARTS skips thoughtSignature on any part, in any contents[] entry. Each of these passes (no 403) with a 600-char base64 value:
{"contents":[{"role":"user","parts":[{"text":"hi","thoughtSignature":"<600 b64>"}]}]}- the same with no
role. Gemini treats a missing role as user. {"contents":[{"role":"user","parts":[{"functionResponse":{"name":"f","response":{"r":"ok"}},"thoughtSignature":"<600 b64>"}]}]}
Only a role: "model" entry holds vendor signatures. This is the Gemini sibling of the earlier anthropic user-part case (#5).
2. Exempt values are not type-checked, so a whole subtree is skipped, binary check included. Both of these pass:
{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"t","signature":{"note":"<600 b64>"}}]}]}{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"t","signature":{"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBO"}}}]}]}
Exempt a sealed field only when its value is a string. Scan or reject anything else.
3. The anthropic field list is wider than the protocol. thinking and redacted_thinking both get {signature, thinkingSignature, data}. Anthropic has signature on thinking and data on redacted_thinking, nothing more. So {"type":"thinking","thinking":"t","signature":"s","data":"<600 b64>"} passes. Please list the exact pairs. If DSH's pi-ai shape really needs thinkingSignature on the chat side, keep it there only.
4. Copilot's bounded-scan point is real, and it falls on the newest content. The walk goes oldest-first, so in a long session the scanChars budget (200k) is spent on history, and the turn just added is never scanned:
- 60 ordinary turns (~249k chars), then
{"role":"user","content":"newest: <600 b64>"}: passes on anthropic and chat. The only trace is the log line "text scan hit its character budget". - one 333k-char
tool_resultwith the blob in the middle: passes.
Most agent sessions pass 200k chars quickly, so the blob check stops working for the requests that matter. The registry summary also says it "fails closed on content it cannot inspect", which this contradicts. One fix that keeps the bound: walk messages / input / contents newest-first, so the budget goes to what is new. Earlier turns were inspected when they were the newest. Whatever you choose, the summary and README must say what is left unscanned.
5. In magpie, a hook that runs out of time fails open, and the defaults let it. magpie stops a hook after 250 ms and sends the request as it was (internal/middleware/middleware.go, requestLimit). On mote (M5 Pro):
- 30,000 one-part messages (~90k nodes) with a blob in the last one took 260 ms. The log said
onRequest: took too long, and the request went through.maxNodesdefaults to 200,000, so the 403 forINSPECT_LIMITis never reached before the timeout. Slower machines will hit this much earlier. - 200k chars made of 399-char base64 or hex runs (just under the threshold) took 300 to 340 ms because the unanchored
{400,}regexes backtrack. That also timed out and went through, a blob after them included. - For scale, an ordinary Claude-Code-shaped body of 2,000 tool turns (2.4 MB) took 111 ms.
Please set maxNodes to a size measured in moejs with room to spare on a slow machine, and count base64/hex run lengths in one linear pass instead of the {400,} regexes. A case in cases.json for each of 1 to 4 would hold them. For 5, a timing note or a moejs measurement in the PR body is enough.
The package shape, registry entry and check script are fine; no network, timers or imports.
9db4a85 to
a51ffb7
Compare
|
All five points are addressed in 1. Gemini role. Only 2. Sealed values must be strings. A sealed key is skipped only when its value is a string. Object values are walked normally, so a 3. Exact Anthropic fields. 4. Budget goes to newest content. Protocol arrays are walked newest-first and the root content array is visited before 5. Timeout/perf. Measured on this machine (Node 24.21 / Bun 1.4.2; I don't have moejs locally, so these are conservative host numbers):
|
yetone
left a comment
There was a problem hiding this comment.
Thanks. I re-reviewed at a51ffb7c2e60e08f91218f260af0737d5b545233, read every changed line of privacy-gate.middleware.js, README, package.json, cases.json and the registry entry, and ran it again in magpie's own host (moejs, internal/middleware Begin/Request, default options, model deepseek/x) on mote (M5 Pro). Points 1 to 3 are fixed, and so is most of 4. Point 5 is only half fixed, so I'm not merging yet.
Fixed (all checked in moejs)
- Gemini:
thoughtSignature/textSignatureon a user-role part, a part with no role and a userfunctionResponsepart: 403. A model-rolefunctionCallpart with a stringthoughtSignature: passes. A blob inside model-rolefunctionCall.args: 403. - A sealed key whose value is an object or array (
signature: {note},signature: {type:"image",…},signature: [b64], Responsesencrypted_content: {…}): 403. String values still pass. thinkingwithdataorthinkingSignature,redacted_thinkingwithsignature, chatthinkingwithsignatureordata: 403. Anthropicredacted_thinking.dataand chatthinkingSignature: pass.- 60 turns (~240 KB) and then a blob in the newest turn: 403 on anthropic, chat, responses and gemini.
- (part) The run-length counters are linear. 200 KB of 399-char base64 or hex runs takes 19–23 ms, down from 300–340 ms. 30,000 one-part messages with the blob in the oldest: 403 in 110 ms.
The 8 new cases in cases.json are red against 9db4a85's middleware and green at this head. bun test packages/privacy-gate: 41 pass. bun scripts/check.mjs: exit 0. On the PR merged onto main (8765cf8), whole-repo bun test with a sandbox HOME gives 6141 pass and 3 fail. All 3 are agent-office jump tests, unrelated (#642); they pass alone on main.
Still wrong
A. Strings, numbers and booleans don't count toward maxNodes, so the walk still has no bound, and a timeout still fails open. walkValue counts only objects and arrays. An array of primitives costs walk time but no nodes:
| body (anthropic, default options) | time in moejs | result |
|---|---|---|
oldest message has an image part; newest tool_use.input = {"v": [0..3,000,000]} (22 MB) |
316 ms | onRequest: took too long, sent with the image |
| same, with a 600-char base64 blob in the oldest message instead of the image | 328 ms | took too long, sent with the blob |
newest tool_use.input = 1,500,000 one-char strings, image in the oldest |
235 ms | 403, with 15 ms to spare on this machine |
Please count every value the walk visits (array elements and primitives too) toward maxNodes, or add a separate value bound. A case for this needs a timing harness rather than cases.json, so a note in the PR is enough.
B. The default bound still doesn't fit in 250 ms with room to spare. The walk costs about 5 µs per object in moejs. With a no-op hook, the same bodies take 3–4 ms, so the time is the walk, not magpie's parse.
| body | no-op hook | privacy-gate |
|---|---|---|
Claude-Code-shaped session: 2,000 tool turns (text + tool_use, tool_result with a text part and cache_control), ~22k objects, 2.9 MB; allowed |
3.9 ms | 145 ms |
| 13,000 messages with one text part each (~39k objects), blob in the oldest | 2.5 ms | 109 ms |
So an ordinary long session uses 58% of the hook's budget on an M5 Pro. A machine about 1.7× slower times out on it, and the gate then lets everything through: blobs, images and models outside the allowlist. Your Node/Bun numbers (4–19 ms) don't carry over to moejs. Please measure in magpie's host. The quickest way is a Go test in internal/middleware that loads the file with plugin.Entry{Spec: path} and times Begin(...).Request(body); I can share mine. Then make the walk cheaper, or lower maxNodes, so the default worst case stays well under the limit on a slow machine (say under 100 ms on an M5 Pro). If a lower maxNodes means long sessions get a 403, the README should say at what size.
C. Within a turn, the first long field takes the whole remaining budget. Parts inside one message are still walked first to last, and a field longer than the remaining budget is edge-scanned with all of it. This newest message passes, logging "text scan hit its character budget":
{"messages":[ …60 turns…, {"role":"user","content":[{"type":"text","text":"<205k chars of prose>"},{"type":"text","text":"newest: <600 b64>"}]}]}A pasted log followed by a pasted key is a realistic shape. The README names only "the middle of an enormous field" and "older history". Please cap one field's share of the budget, so the newest turn's later parts are still scanned, or say this in the README. The README should also say that system / instructions are scanned after the conversation: a blob in system behind a 240 KB history passes.
Smaller: the README's "Gemini part thoughtSignature" should say model-role part.
Once A and B are fixed, with moejs numbers in the PR, I'll re-run all of the above.
|
A DSH-side layer is now implemented as well, so DSH traffic can be blocked before Magpie. Commit
It rejects image/file/audio/video blocks, unknown block types, surviving PII or BLOB in text/reasoning/tool-call arguments/tool-result content, disallowed provider/model, and over-budget requests (fail closed; no degraded pass). It does not rewrite the frozen request. Live test against Magpie (DSH 0.2.1-rc.5 + Magpie 0.1.3038):
Also fixed a bundle-activation issue found while testing: the tool output schemas used per-property This does not close magpie#5305 for other clients: Cursor/Copilot/scripts still rely on Magpie, so the engine-side gate mode / longer budget is still needed. But DSH traffic now has a fail-closed boundary that does not depend on Magpie's hook timeout. The DSH bundle |
|
Fixed A, B and the smaller C wording in A — every visited value is now bounded. B — measured in moejs, not Node/Bun. I loaded the candidate into the real Magpie gateway, instrumented the hook with
The previous 2,000-turn body measured 145 ms in moejs; the value/width checks plus direct binary-key checks bring the larger 3.5 MB body to 76 ms, under the 100 ms target. Since 40,000 values is reached, a 2,000-tool-turn body is refused, not silently passed; README now states that size and the ~38 ms passing 200-turn baseline. The default bound can still be tuned, but that is not what makes the timeout safe — see below. C — per-field text share. Timeout escape is not a middleware-solvable guarantee. The middleware can bound its own work, but it cannot make a host timeout fail closed: when moejs cuts the hook off, the middleware is not resumed, cannot call 43/43 cases pass; |
yetone
left a comment
There was a problem hiding this comment.
Thanks. I re-reviewed at 2d16b15d9c321614621f7ed6e21128f9e55385bc, read the whole diff since a51ffb7c and the walk again, and re-ran everything in magpie's own host (moejs, internal/middleware Begin/Request, default options, model deepseek/x) on mote (M5 Pro), with 22 new cases on top of the earlier 40.
Fixed (checked in moejs)
- A. Primitives and array elements count. 3M numbers in the newest
tool_use.inputwith an image or blob in the oldest message: 403 in 48–55 ms (was "took too long" at 316–328 ms and sent). 1.5M one-char strings: 403 in 21–23 ms. - B. The default worst case fits with room. Claude-Code-shaped sessions: 100 tool turns 30 ms, 200 → 53 ms, 400 → 61 ms, 1,000 → 61 ms, 1,500 → 70 ms (all pass); 2,000 turns (2.9 MB) 74 ms and 403 at the value cap, as the README now says. Worst shapes I could build near the cap: 39,990 empty objects 32 ms, 19,990 one-key objects 21 ms, 6,600 one-part messages 22 ms, 39,000 short strings 11 ms. Nothing came near 250 ms.
- C. A 205k-char first part followed by a newest-part blob: 403 at default options. The README now names
system/instructionsand the per-field share. - Points 1–5 from the earlier rounds still hold (all 40 earlier cases as before).
- The README's "model-role part" wording is fixed. The new cases in cases.json are fine.
binaryOf's inlined keys are the same asSTRONG_KEY/WEAK_KEY.
Still wrong
D. SKIP_KEY is matched by key name at every depth, so a declaration-named key inside model- or user-written content is never walked. walkValue and findFirstBinary both continue on tools, parameters, input_schema, generationConfig and the rest wherever they appear. Each of these passes with a 600-char base64 blob (or an image):
| body | result |
|---|---|
anthropic tool_use.input = {"parameters": "<blob>"} |
pass |
anthropic tool_use.input = {"tools": {"x": "<blob>"}} |
pass |
anthropic tool_use.input = {"input_schema": {"type":"image","source":{…base64…}}} |
pass (also through onProvider) |
anthropic tool_result part with "parameters": "<blob>" |
pass |
gemini model-role functionCall.args = {"parameters": "<blob>"} |
pass |
gemini user functionResponse.response = {"generationConfig": "<blob>"} |
pass |
responses user message with "parameters": "<blob>" |
pass |
Tool arguments are written by the model and tool results come from the user's files, so {"parameters": …} there is ordinary data. This is the same class as the earlier signature/id exemptions: an exemption has to come from where the value sits in the protocol, not from its key's name. The declarations live at the body root (tools, tool_choice, functions, response_format, toolConfig, generationConfig, …; parameters / input_schema / json_schema only under those). Please skip them only at SITE_ROOT (and under a skipped root key, which is already never walked), in both walkers, and add a case for each row above. I missed this in the earlier rounds; sorry for the extra one.
Smaller, no change needed unless you want it: a field longer than max(1024, scanChars/8) (25k by default) is now scanned only at its edges, so a blob in the middle of a 30k tool result passes where it was caught before. The README says so; just noting the trade.
Once D is fixed I'll re-run the whole set and merge.
|
D is fixed in Root-only declarations. Seven cases added, one per row in the review: anthropic No changes to |
Review at 43ae7c4 (privacy-gate 0.1.0)Thanks for the A–D fixes. They hold up. The new Earlier points: all addressed
Blockers in 43ae7c4
Smaller points
What was run at 43ae7c4All runs were on mote (the macOS test box) through mote-run, with
Not tried
中文总结A–D 四点都已修好,复测全部符合预期。新提交 43ae7c4(binaryPolicy)有两个会让门失效(fail-open)的问题,所以暂不合并。
|
43ae7c4 to
ab8c0eb
Compare
yetone
left a comment
There was a problem hiding this comment.
Review at ab8c0eb (privacy-gate 0.1.0)
Thanks. All four blockers from the last round are fixed, and so are most of the smaller points. One gap of the same kind as blocker 1 is still there, and one guard has no test. So this needs one more round.
Last round's points at this head
- An invalid
binaryPolicyno longer turns the gate off. Fixed.policyOptfalls back tofail, andgateOptions/usageOptionscatch. In magpie's own engine, withbinaryPolicyset to"Guide","GUIDE","block","x",true,1ornull, every case gets 403: an anthropic image, a chat image_url, a gemini inlineData, a blob in text, and a model outsideallowModels. WithlocalModelsset,onProviderno longer throws, so the image doesn't reach a non-local provider. - Guide mode has a bound now. Fixed.
guideWalkmakes one pass that counts towardmaxNodes, and the secondinspectis gone. A newest image plus 3M numbers in an oldertool_use.inputnow gets 403 in 120 ms. Before it took 346 to 378 ms, the hook timed out and the bytes went out. 1,500 Claude Code turns plus a newest image pass in 144 ms. Load on mote was around 93, so these timings run high. - 422 vs 403. Fixed. The header, the README and registry.json (en and zh) all say 403 now. A grep for 422 finds nothing left.
- registry.json. Fixed. The PR merges cleanly onto origin/main 654d3be. Whole
bun scripts/check.mjsexits 0.
The smaller points are fixed too. Guide mode uses the inspector's protocol sites, so tool_use.input.content:[{"file":…}] is left alone. The vendor prompt no longer names ShyPie, and guideTools defaults to []. guideMime is clipped and must be a plain type/subtype, so 9.6 KB of prose in media_type gives no mime at all. A Responses input_image given as a string now carries its mime and size.
Still blocking
- A typo in
allowModelsturns the model allowlist off.strListreturns[]for anything that isn't an array of short strings, and[]means "no allowlist".allowModels: "local/*"(a string where a list should be) andallowModels: [{"glob":"local/*"}]both let every model through. I checked this in magpie's engine:deepseek/xpassed with each one, and got 403 with["local/*"]. This is the rule from blocker 1 ("a typo in an option should never mean no gate") applied to the one option that loosens the gate when it is missing. WhenallowModelsis present but not a usable list (not an array, or one entry dropped), refuse every request with a message that names the option. Don't fall back to no allowlist.localModels,localProviders,allowUnknownProtocoland the numeric options all fall back to the stricter side, so they're fine as they are. - Nothing tests guide mode's blob scan. Once guide mode is on,
inspectstops at the newest binary part. Since the secondinspectis gone, thescanTextcall inguideWalkis the only thing that checks the older turns. I broke it by hand, withguideWalkreturning strings unscanned, and all 56 package tests still pass. In magpie's engine, three bodies then went out with their bytes: a blob in an older user text, a blob in an oldertool_use.input, and adata:image/png;base64,…insystem. Each has a newest image. Please add those three ascases.jsoncases withbinaryPolicy: "guide"and a 403 expected.
Smaller points (not blocking)
- The new mime check has no case either. With the regex removed,
return candidates[i], the package tests stay green. A case with prose or a blob inmedia_typewould hold it. - The node bound in
guideWalkis checked twice, per value and per array or key count, so removing either one alone leaves "guide: maxNodes refuses a body it cannot bound" green. Only removing both turns it red. That's fine as defence in depth. Just know the test covers the pair, not each check. - In guide mode, a
tool_use.inputobject that has animage_urlkey is replaced whole by a text part, so the tool call'sinputbecomes{"type":"text","text":"[A binary attachment…]"}. No bytes go out, so this is safe. It does change the shape of a model-written argument, though, and some vendors may refuse it. Fail mode gives 403 here. DEFAULTS.guideToolsis never read now.resolvedOptionsusesstrListdirectly.
What was run at ab8c0eb
All runs were on mote (the macOS test box) through mote-run, with HOME=$(mktemp -d) and Bun 1.4.2, on origin/main 654d3be merged with this head (no conflicts).
bun test packages/privacy-gate: 56 pass, 0 fail.bun scripts/check.mjs privacy-gate: ok. Wholebun scripts/check.mjs: exit 0.- magpie's own engine (internal/middleware, moejs, magpie origin/main 8c97edc80), with the PR file byte for byte (sha1 fb1d7bef): every earlier case (A–D, the 20 binaryPolicy cases, onProvider) plus 14 new ones. The cases not as wanted were the two
allowModelstypos above. One more case changed: "blob as media_type" in guide mode now passes where it used to get 403, but the blob is dropped with the part and nothing is sent, so that's correct. The ~2,000-turn Claude Code case gets 403 at the value cap, as the README says. - Broken by hand, each change run alone against the package tests:
policyOptthrowing and no catch ingateOptions: "invalid binaryPolicy falls back to fail" goes red. With the catch kept, it stays green, since there are two layers.- both node bounds removed from
guideWalk: "guide: maxNodes refuses…" goes red. binaryOf(value, true)inguideWalk: "guide: tool_use.input is not rewritten…" goes red.- a
shypie.*default put back inresolvedOptions: 4 guide cases go red. - string
image_urlhandling removed: "Responses input_image string…" goes red. - the blob scan in
guideWalkremoved: still green (point 2 above). - the mime regex removed: still green.
- Version:
npm view --prefer-online --registry https://registry.npmjs.org @magpie-community/middleware-privacy-gatereturns 404, and the dist branch's index.json doesn't list it, so 0.1.0 is free. - Look-for-harm: no network access, imports, eval,
processorBunin the middleware. The test file only importsscripts/middleware.mjs.
Not tried
- A real upstream vendor, real DSH, or a local MCP extraction tool.
- Streaming bodies.
- A machine with low load (the timeout margins will differ).
- Windows and Linux.
中文总结
上一轮的四个阻塞点在 ab8c0eb 上都已修好。binaryPolicy 填错时回退为 fail,guide 模式受 maxNodes 限制(3M 数字的请求现在 120 ms 内返回 403,图片不再外发),文档统一成了 403,registry.json 也已无冲突。几个小问题同样修好了:提示文字里不再出现 ShyPie,guideTools 默认为空,mime 会截断并校验,工具参数不会再被改写。
还剩两点需要处理。
- allowModels 填错(比如写成字符串 "local/*",或者列表里放的是对象)时,模型允许列表会被整个关掉,任何模型都能通过。这和上一轮第 1 点属于同一类问题,填错时应该拒绝所有请求。
- guide 模式下,较旧消息里的编码数据块只靠 guideWalk 里的 scanText 拦截。我手动去掉这一步后,包内测试仍然全部通过,在 magpie 引擎里实测有三种请求会把字节原样发出。请把这三种情况补进 cases.json。
包测试 56 项通过,check.mjs 通过,0.1.0 版本号可用。
ab8c0eb to
2a998b7
Compare

Adds ShyPie Privacy Gate as in-tree gateway middleware, per the review.
Source moved here. The middleware lives at
packages/privacy-gate/:privacy-gate.middleware.js— one moejs-compatible file, no imports/network/timers;cases.json— 41 cases, including the protocol-site bypasses, Gemini role and sealed-type regressions, and the newest-content budget case;privacy-gate.test.mjs—check(import.meta.dir, mod)throughscripts/middleware.mjs;package.json,README.md,LICENSE.The registry entry points at
@magpie-community/middleware-privacy-gatewith"community": true; the external@shypie_privacy/magpie-privacy-gatepackage is no longer referenced.Scope. The Magpie half of ShyPie's privacy-first workflow for DeepSeek Harness: DSH does ingestion masking and local extraction, this gate enforces the structural egress half with DSH on DeepSeek models behind Magpie's gateway. It also applies to any agent behind the gateway. It does not mask text (Magpie's Privacy masking does). Image/file/audio parts are 403 by default and pass only when both
localModelsandlocalProvidersagree; withlocalModelsalone the request is not sent butonProviderblanks the content. The scan is bounded, so long encoded tokens can be false positives.Vendor signatures, scoped by protocol site. The exemption is now assigned only where the protocol puts a vendor block:
messages[].content[]thinking blocks;input[]reasoning items (encrypted_content,id) and assistant messageid;contents[].parts[](thoughtSignature/textSignature);messages[].content[]thinking blocks and tool calls.tool_use.input,tool_result.content,function_call.argumentsand any array reached through another object are scanned as ordinary content. The type-less fallback branch is gone, andidis limited to assistant/vendor items (a user Responses message id is scanned).Verification.
bun test packages/privacy-gate→ 43/43 pass;bun scripts/check.mjs→ exit 0;registryProblems(registry.json)→ no problems. The six bypasses from the review are cases that must 403, alongside the positive signature cases. Live Copilot-routed OpenAI/Anthropic signature replays and the DSH two-turn session were verified earlier.Latest review A/B/C —
2d16b15.maxNodescounts every visited value, including primitives and array elements; an array or object wider than the remaining budget is refused without iterating. The structuralonProviderwalker has the same bound.maxNodesremains 40,000.max(1024, scanChars/8), so one long field cannot starve the next part;system/instructionsare visited after conversation content and README says so. 43 cases include the primitive-array and long-first-field regressions.Timeout is engine policy, not a middleware guarantee. A middleware cannot make a host timeout fail closed: when moejs stops the hook, the middleware is not resumed, cannot call
ctx.reject, and cannot return a body, so Magpie sends the original request. The bound above keeps the default shapes under 100 ms, but no amount of JS can turn a killed hook into a verdict. The correct fix is a host-level gate timeout rejection (or a declared fail-closed gate budget); that remains issue #5305 and is outside this middleware. The README states the distinction rather than claiming the timeout is solved.Companion DSH bundle: https://github.com/siml1169/ShyPie/tree/main/plugins/dsh-privacy-gate