@intutic/sync-daemon 2.2.0 → 2.4.0

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 (235) hide show
  1. package/README.md +1 -1
  2. package/dist/agentReporter.d.ts +23 -11
  3. package/dist/agentReporter.d.ts.map +1 -1
  4. package/dist/agentReporter.js +20 -13
  5. package/dist/agentReporter.js.map +1 -1
  6. package/dist/configReader.d.ts +1 -3
  7. package/dist/configReader.d.ts.map +1 -1
  8. package/dist/configReader.js +0 -2
  9. package/dist/configReader.js.map +1 -1
  10. package/dist/configWriter.d.ts +11 -12
  11. package/dist/configWriter.d.ts.map +1 -1
  12. package/dist/configWriter.js +35 -58
  13. package/dist/configWriter.js.map +1 -1
  14. package/dist/disconnect/harnesses.d.ts +0 -2
  15. package/dist/disconnect/harnesses.d.ts.map +1 -1
  16. package/dist/disconnect/harnesses.js +157 -80
  17. package/dist/disconnect/harnesses.js.map +1 -1
  18. package/dist/disconnect/index.d.ts.map +1 -1
  19. package/dist/disconnect/index.js +11 -1
  20. package/dist/disconnect/index.js.map +1 -1
  21. package/dist/disconnect/mcp.d.ts +21 -0
  22. package/dist/disconnect/mcp.d.ts.map +1 -1
  23. package/dist/disconnect/mcp.js +71 -0
  24. package/dist/disconnect/mcp.js.map +1 -1
  25. package/dist/disconnect/recognise.d.ts +2 -2
  26. package/dist/disconnect/recognise.d.ts.map +1 -1
  27. package/dist/disconnect/recognise.js +15 -5
  28. package/dist/disconnect/recognise.js.map +1 -1
  29. package/dist/harness/aiderConfigMerger.d.ts +28 -8
  30. package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
  31. package/dist/harness/aiderConfigMerger.js +59 -11
  32. package/dist/harness/aiderConfigMerger.js.map +1 -1
  33. package/dist/harness/antigravityCliHooks.d.ts +61 -0
  34. package/dist/harness/antigravityCliHooks.d.ts.map +1 -0
  35. package/dist/harness/antigravityCliHooks.js +207 -0
  36. package/dist/harness/antigravityCliHooks.js.map +1 -0
  37. package/dist/harness/antigravityHooks.d.ts +12 -2
  38. package/dist/harness/antigravityHooks.d.ts.map +1 -1
  39. package/dist/harness/antigravityHooks.js +36 -16
  40. package/dist/harness/antigravityHooks.js.map +1 -1
  41. package/dist/harness/antigravityProducts.d.ts +40 -0
  42. package/dist/harness/antigravityProducts.d.ts.map +1 -0
  43. package/dist/harness/antigravityProducts.js +109 -0
  44. package/dist/harness/antigravityProducts.js.map +1 -0
  45. package/dist/harness/claudeAgentsMd.d.ts +29 -0
  46. package/dist/harness/claudeAgentsMd.d.ts.map +1 -0
  47. package/dist/harness/claudeAgentsMd.js +80 -0
  48. package/dist/harness/claudeAgentsMd.js.map +1 -0
  49. package/dist/harness/claudeCodeHooks.d.ts +6 -5
  50. package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
  51. package/dist/harness/claudeCodeHooks.js +98 -24
  52. package/dist/harness/claudeCodeHooks.js.map +1 -1
  53. package/dist/harness/clineHooks.d.ts +0 -1
  54. package/dist/harness/clineHooks.d.ts.map +1 -1
  55. package/dist/harness/clineHooks.js +10 -4
  56. package/dist/harness/clineHooks.js.map +1 -1
  57. package/dist/harness/codexHooks.d.ts +2 -0
  58. package/dist/harness/codexHooks.d.ts.map +1 -1
  59. package/dist/harness/codexHooks.js +12 -4
  60. package/dist/harness/codexHooks.js.map +1 -1
  61. package/dist/harness/cursorHooks.d.ts +5 -2
  62. package/dist/harness/cursorHooks.d.ts.map +1 -1
  63. package/dist/harness/cursorHooks.js +9 -3
  64. package/dist/harness/cursorHooks.js.map +1 -1
  65. package/dist/harness/dshHooks.d.ts +5 -5
  66. package/dist/harness/dshHooks.d.ts.map +1 -1
  67. package/dist/harness/dshHooks.js +5 -5
  68. package/dist/harness/gateArtifacts.d.ts +35 -0
  69. package/dist/harness/gateArtifacts.d.ts.map +1 -0
  70. package/dist/harness/gateArtifacts.js +88 -0
  71. package/dist/harness/gateArtifacts.js.map +1 -0
  72. package/dist/harness/gateBody.d.ts +186 -69
  73. package/dist/harness/gateBody.d.ts.map +1 -1
  74. package/dist/harness/gateBody.js +820 -182
  75. package/dist/harness/gateBody.js.map +1 -1
  76. package/dist/harness/gateSightings.d.ts +47 -0
  77. package/dist/harness/gateSightings.d.ts.map +1 -0
  78. package/dist/harness/gateSightings.js +159 -0
  79. package/dist/harness/gateSightings.js.map +1 -0
  80. package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
  81. package/dist/harness/githubCopilotHooks.js +7 -1
  82. package/dist/harness/githubCopilotHooks.js.map +1 -1
  83. package/dist/harness/gooseHardener.d.ts +0 -1
  84. package/dist/harness/gooseHardener.d.ts.map +1 -1
  85. package/dist/harness/gooseHardener.js +1 -1
  86. package/dist/harness/gooseHardener.js.map +1 -1
  87. package/dist/harness/gooseHooks.d.ts +14 -1
  88. package/dist/harness/gooseHooks.d.ts.map +1 -1
  89. package/dist/harness/gooseHooks.js +55 -29
  90. package/dist/harness/gooseHooks.js.map +1 -1
  91. package/dist/harness/grokHooks.d.ts +2 -2
  92. package/dist/harness/grokHooks.d.ts.map +1 -1
  93. package/dist/harness/grokHooks.js +11 -6
  94. package/dist/harness/grokHooks.js.map +1 -1
  95. package/dist/harness/hermesHooks.d.ts +18 -8
  96. package/dist/harness/hermesHooks.d.ts.map +1 -1
  97. package/dist/harness/hermesHooks.js +48 -17
  98. package/dist/harness/hermesHooks.js.map +1 -1
  99. package/dist/harness/mcpAutoWrite.d.ts +24 -9
  100. package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
  101. package/dist/harness/mcpAutoWrite.js +218 -25
  102. package/dist/harness/mcpAutoWrite.js.map +1 -1
  103. package/dist/harness/museHooks.d.ts +3 -3
  104. package/dist/harness/museHooks.d.ts.map +1 -1
  105. package/dist/harness/museHooks.js +9 -5
  106. package/dist/harness/museHooks.js.map +1 -1
  107. package/dist/harness/n8nHooks.d.ts +0 -1
  108. package/dist/harness/n8nHooks.d.ts.map +1 -1
  109. package/dist/harness/n8nHooks.js +4 -1
  110. package/dist/harness/n8nHooks.js.map +1 -1
  111. package/dist/harness/openCodeHooks.d.ts +2 -2
  112. package/dist/harness/openCodeHooks.d.ts.map +1 -1
  113. package/dist/harness/openCodeHooks.js +7 -3
  114. package/dist/harness/openCodeHooks.js.map +1 -1
  115. package/dist/harness/openWebuiHooks.d.ts +0 -1
  116. package/dist/harness/openWebuiHooks.d.ts.map +1 -1
  117. package/dist/harness/openWebuiHooks.js +5 -1
  118. package/dist/harness/openWebuiHooks.js.map +1 -1
  119. package/dist/harness/openclawHooks.d.ts +70 -18
  120. package/dist/harness/openclawHooks.d.ts.map +1 -1
  121. package/dist/harness/openclawHooks.js +213 -161
  122. package/dist/harness/openclawHooks.js.map +1 -1
  123. package/dist/harness/openhandsHooks.d.ts +24 -10
  124. package/dist/harness/openhandsHooks.d.ts.map +1 -1
  125. package/dist/harness/openhandsHooks.js +49 -48
  126. package/dist/harness/openhandsHooks.js.map +1 -1
  127. package/dist/harness/piHooks.d.ts +52 -17
  128. package/dist/harness/piHooks.d.ts.map +1 -1
  129. package/dist/harness/piHooks.js +177 -166
  130. package/dist/harness/piHooks.js.map +1 -1
  131. package/dist/harness/protectedPaths.d.ts +107 -25
  132. package/dist/harness/protectedPaths.d.ts.map +1 -1
  133. package/dist/harness/protectedPaths.js +342 -48
  134. package/dist/harness/protectedPaths.js.map +1 -1
  135. package/dist/harness/rulesSection.d.ts +62 -0
  136. package/dist/harness/rulesSection.d.ts.map +1 -0
  137. package/dist/harness/rulesSection.js +145 -0
  138. package/dist/harness/rulesSection.js.map +1 -0
  139. package/dist/harness/windsurfHooks.d.ts +0 -1
  140. package/dist/harness/windsurfHooks.d.ts.map +1 -1
  141. package/dist/harness/windsurfHooks.js +4 -1
  142. package/dist/harness/windsurfHooks.js.map +1 -1
  143. package/dist/index.d.ts +12 -8
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +11 -9
  146. package/dist/index.js.map +1 -1
  147. package/dist/inventory.d.ts +53 -0
  148. package/dist/inventory.d.ts.map +1 -0
  149. package/dist/inventory.js +133 -0
  150. package/dist/inventory.js.map +1 -0
  151. package/dist/lib/complianceProbes.d.ts +2 -1
  152. package/dist/lib/complianceProbes.d.ts.map +1 -1
  153. package/dist/lib/complianceProbes.js +1 -1
  154. package/dist/lib/complianceProbes.js.map +1 -1
  155. package/dist/lib/decisionsDigest.d.ts +34 -16
  156. package/dist/lib/decisionsDigest.d.ts.map +1 -1
  157. package/dist/lib/decisionsDigest.js +105 -46
  158. package/dist/lib/decisionsDigest.js.map +1 -1
  159. package/dist/lib/egressPolicy.d.ts +2 -2
  160. package/dist/lib/egressPolicy.js +2 -2
  161. package/dist/lib/mcpRegistryPy.d.ts +11 -0
  162. package/dist/lib/mcpRegistryPy.d.ts.map +1 -0
  163. package/dist/lib/mcpRegistryPy.js +173 -0
  164. package/dist/lib/mcpRegistryPy.js.map +1 -0
  165. package/dist/lib/phrasesPy.d.ts +11 -0
  166. package/dist/lib/phrasesPy.d.ts.map +1 -0
  167. package/dist/lib/phrasesPy.js +351 -0
  168. package/dist/lib/phrasesPy.js.map +1 -0
  169. package/dist/lib/policySnapshot.d.ts +46 -10
  170. package/dist/lib/policySnapshot.d.ts.map +1 -1
  171. package/dist/lib/policySnapshot.js +136 -47
  172. package/dist/lib/policySnapshot.js.map +1 -1
  173. package/dist/lib/processPoller.d.ts.map +1 -1
  174. package/dist/lib/processPoller.js +9 -2
  175. package/dist/lib/processPoller.js.map +1 -1
  176. package/dist/lib/regexLinearity.d.ts +43 -0
  177. package/dist/lib/regexLinearity.d.ts.map +1 -0
  178. package/dist/lib/regexLinearity.js +275 -0
  179. package/dist/lib/regexLinearity.js.map +1 -0
  180. package/dist/lib/runtimeEnv.d.ts +1 -3
  181. package/dist/lib/runtimeEnv.d.ts.map +1 -1
  182. package/dist/lib/runtimeEnv.js +0 -2
  183. package/dist/lib/runtimeEnv.js.map +1 -1
  184. package/dist/lib/sequencePy.d.ts +10 -0
  185. package/dist/lib/sequencePy.d.ts.map +1 -0
  186. package/dist/lib/sequencePy.js +102 -0
  187. package/dist/lib/sequencePy.js.map +1 -0
  188. package/dist/sessionReporter.d.ts +17 -11
  189. package/dist/sessionReporter.d.ts.map +1 -1
  190. package/dist/sessionReporter.js +36 -23
  191. package/dist/sessionReporter.js.map +1 -1
  192. package/dist/skillWriter.d.ts +23 -5
  193. package/dist/skillWriter.d.ts.map +1 -1
  194. package/dist/skillWriter.js +159 -16
  195. package/dist/skillWriter.js.map +1 -1
  196. package/dist/syncCycle.d.ts.map +1 -1
  197. package/dist/syncCycle.js +21 -9
  198. package/dist/syncCycle.js.map +1 -1
  199. package/dist/trajectoryMonitor.d.ts +1 -3
  200. package/dist/trajectoryMonitor.d.ts.map +1 -1
  201. package/dist/trajectoryMonitor.js +1 -4
  202. package/dist/trajectoryMonitor.js.map +1 -1
  203. package/dist/watcher/driftWatcher.d.ts +0 -1
  204. package/dist/watcher/driftWatcher.d.ts.map +1 -1
  205. package/dist/watcher/driftWatcher.js +1 -2
  206. package/dist/watcher/driftWatcher.js.map +1 -1
  207. package/dist/watcher/settingsGuard.d.ts +33 -7
  208. package/dist/watcher/settingsGuard.d.ts.map +1 -1
  209. package/dist/watcher/settingsGuard.js +373 -61
  210. package/dist/watcher/settingsGuard.js.map +1 -1
  211. package/dist/watcher/vscodeHookSettings.d.ts +30 -0
  212. package/dist/watcher/vscodeHookSettings.d.ts.map +1 -0
  213. package/dist/watcher/vscodeHookSettings.js +170 -0
  214. package/dist/watcher/vscodeHookSettings.js.map +1 -0
  215. package/dist/wsClient.d.ts +0 -1
  216. package/dist/wsClient.d.ts.map +1 -1
  217. package/dist/wsClient.js +0 -1
  218. package/dist/wsClient.js.map +1 -1
  219. package/package.json +5 -5
  220. package/dist/harness/claudeDesktopHooks.d.ts +0 -33
  221. package/dist/harness/claudeDesktopHooks.d.ts.map +0 -1
  222. package/dist/harness/claudeDesktopHooks.js +0 -256
  223. package/dist/harness/claudeDesktopHooks.js.map +0 -1
  224. package/dist/harness/continueHooks.d.ts +0 -63
  225. package/dist/harness/continueHooks.d.ts.map +0 -1
  226. package/dist/harness/continueHooks.js +0 -250
  227. package/dist/harness/continueHooks.js.map +0 -1
  228. package/dist/harness/gateKind.d.ts +0 -164
  229. package/dist/harness/gateKind.d.ts.map +0 -1
  230. package/dist/harness/gateKind.js +0 -144
  231. package/dist/harness/gateKind.js.map +0 -1
  232. package/dist/harness/rooCodeHooks.d.ts +0 -31
  233. package/dist/harness/rooCodeHooks.d.ts.map +0 -1
  234. package/dist/harness/rooCodeHooks.js +0 -281
  235. package/dist/harness/rooCodeHooks.js.map +0 -1
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The MCP server registry and allowlist decisions in Python, for the bash
3
+ * gates (their extractor already requires python3).
4
+ *
5
+ * Byte-identical to `packages/intutic-clawde/intutic_clawde/gate/mcp_registry.py`,
6
+ * which a test compares; edit that file and copy it here. It is the
7
+ * transliteration of `@intutic/shared-types`'s mcpRegistryRecord.ts, and both
8
+ * run the shared vectors.
9
+ */
10
+ export const MCP_REGISTRY_PY_SOURCE = String.raw `"""The MCP server registry and allowlist decisions, read from the policy snapshot.
11
+
12
+ A transliteration of packages/shared-types/src/mcpRegistryRecord.ts: the
13
+ registry decision (evaluateMcpRegistry), the allowlist decision
14
+ (evaluateMcpAllowlist), and the snapshot's @mcp_registry and @mcp_allowlist
15
+ records. The control plane's hook gate, the MCP proxy and the JavaScript hook
16
+ gates run the TypeScript; the bash hook gates and this package run this file,
17
+ and every one of them runs packages/shared-types/fixtures/mcp-registry-vectors.json,
18
+ so all of them refuse a call with the same code, rule id and reason.
19
+
20
+ The bash gates emit this file verbatim (services/sync-daemon/src/lib/mcpRegistryPy.ts
21
+ holds a byte-identical copy, which a test compares). It must stay
22
+ self-contained: the standard library only, no module state beyond the
23
+ constants below, and no backtick or dollar-brace, which the TypeScript copy
24
+ cannot hold.
25
+
26
+ Both records sit inside the snapshot's digest. A gate that finds the snapshot
27
+ unverified (its digest broken or missing, or another workspace's) trusts
28
+ neither: it refuses every MCP call with unverified_refusal, at block, in an
29
+ observe-only workspace too.
30
+ """
31
+
32
+ import base64
33
+ import json
34
+
35
+ MCP_REGISTRY_RECORD_TAG = "@mcp_registry"
36
+ MCP_ALLOWLIST_RECORD_TAG = "@mcp_allowlist"
37
+
38
+
39
+ def split_mcp_tool_name(tool):
40
+ """(server, tool) for a harness name mcp__<server>__<tool>, else None.
41
+
42
+ The server is the part before the first double underscore; the tool, the
43
+ rest, may hold more of them.
44
+ """
45
+ if not isinstance(tool, str) or not tool.startswith("mcp__"):
46
+ return None
47
+ rest = tool[len("mcp__"):]
48
+ sep = rest.find("__")
49
+ if sep <= 0:
50
+ return None
51
+ return rest[:sep], rest[sep + 2:]
52
+
53
+
54
+ def _strings(value):
55
+ return [v for v in value if isinstance(v, str)] if isinstance(value, list) else []
56
+
57
+
58
+ def parse_registry(value):
59
+ """The registry as the control plane sends it, or None for a non-object.
60
+
61
+ Wrong-typed lists read as empty, and any default but deny as allow.
62
+ """
63
+ if not isinstance(value, dict):
64
+ return None
65
+ disabled = {}
66
+ if isinstance(value.get("disabledTools"), dict):
67
+ for server, tools in value["disabledTools"].items():
68
+ names = _strings(tools)
69
+ if names:
70
+ disabled[server] = names
71
+ return {
72
+ "defaultPolicy": "deny" if value.get("defaultPolicy") == "deny" else "allow",
73
+ "approvedServers": _strings(value.get("approvedServers")),
74
+ "blockedServers": _strings(value.get("blockedServers")),
75
+ "heldServers": _strings(value.get("heldServers")),
76
+ "disabledTools": disabled,
77
+ }
78
+
79
+
80
+ def decode_registry_record(line):
81
+ """The @mcp_registry line's registry; None for another line or a damaged one."""
82
+ parts = line.split("\t")
83
+ if len(parts) != 2 or parts[0] != MCP_REGISTRY_RECORD_TAG or not parts[1]:
84
+ return None
85
+ try:
86
+ return parse_registry(json.loads(base64.b64decode(parts[1], validate=True).decode("utf-8")))
87
+ except Exception:
88
+ return None
89
+
90
+
91
+ def evaluate_registry(registry, server, tool):
92
+ """(code, rule_id, reason) refusing one MCP call, or None to let it continue.
93
+
94
+ A blocked server, then a held one, then (under deny) one not approved,
95
+ then a disabled tool.
96
+ """
97
+ if server in registry["blockedServers"]:
98
+ return (
99
+ "SERVER_BLOCKED",
100
+ "mcp_registry." + server,
101
+ "MCP server \"" + server + "\" is blocked in this workspace's MCP server registry. "
102
+ "An owner or admin can change that on the MCP Servers page.",
103
+ )
104
+ if server in registry["heldServers"]:
105
+ return (
106
+ "SERVER_HELD",
107
+ "mcp_registry." + server,
108
+ "MCP server \"" + server + "\" changed its tools in a way scored high risk, and this workspace "
109
+ "holds such a server until it is approved again. It is waiting in the approval queue on the MCP "
110
+ "Servers page for an owner or admin.",
111
+ )
112
+ if registry["defaultPolicy"] == "deny" and server not in registry["approvedServers"]:
113
+ return (
114
+ "SERVER_NOT_APPROVED",
115
+ "mcpDefaultPolicy",
116
+ "MCP server \"" + server + "\" is not approved in this workspace's MCP server registry, "
117
+ "and the workspace refuses unapproved servers (mcpDefaultPolicy: deny). It is waiting in "
118
+ "the approval queue on the MCP Servers page for an owner or admin.",
119
+ )
120
+ if tool in registry["disabledTools"].get(server, []):
121
+ return (
122
+ "TOOL_DISABLED",
123
+ "mcp_registry." + server + "." + tool,
124
+ "Tool \"" + tool + "\" is disabled on MCP server \"" + server + "\" in this workspace's "
125
+ "MCP server registry. An owner or admin can re-enable it on the MCP Servers page.",
126
+ )
127
+ return None
128
+
129
+
130
+ def decode_allowlist_record(line):
131
+ """The @mcp_allowlist line as {"severity", "servers"}; None for another line.
132
+
133
+ A severity other than shadow reads as block, so a damaged one refuses.
134
+ """
135
+ parts = line.split("\t")
136
+ if len(parts) != 3 or parts[0] != MCP_ALLOWLIST_RECORD_TAG:
137
+ return None
138
+ return {
139
+ "severity": "shadow" if parts[1] == "shadow" else "block",
140
+ "servers": [s for s in parts[2].split(",") if s],
141
+ }
142
+
143
+
144
+ def evaluate_allowlist(allowlist, server):
145
+ """(code, rule_id, reason) for a server the allowlist does not name, else None.
146
+
147
+ Under shadow the caller records the refusal instead of enforcing it.
148
+ """
149
+ if server in allowlist["servers"]:
150
+ return None
151
+ return (
152
+ "SERVER_NOT_ALLOWED",
153
+ "mcp_allowlist",
154
+ "MCP server \"" + server + "\" is not on the MCP server allowlist for this workspace",
155
+ )
156
+
157
+
158
+ def unverified_refusal(server):
159
+ """(code, rule_id, reason) refusing an MCP call on an unverified snapshot.
160
+
161
+ Neither the registry's approvals nor the allowlist's servers can be
162
+ vouched for, and a deleted record looks like one never set, so the
163
+ snapshot admits no MCP server at all.
164
+ """
165
+ return (
166
+ "POLICY_SNAPSHOT_UNVERIFIED",
167
+ "policy_snapshot",
168
+ "MCP server \"" + server + "\" is refused because the policy snapshot on this machine failed its "
169
+ "integrity check, so its MCP server registry and allowlist admit no server until the sync daemon "
170
+ "restores a verified snapshot",
171
+ )
172
+ `;
173
+ //# sourceMappingURL=mcpRegistryPy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpRegistryPy.js","sourceRoot":"","sources":["../../src/lib/mcpRegistryPy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkK/C,CAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The phrase matcher in Python, for the gates that run Python: the bash gates
3
+ * (their extractor already requires python3) and the Open WebUI filter.
4
+ *
5
+ * Byte-identical to `packages/intutic-clawde/intutic_clawde/gate/phrases.py`,
6
+ * which a test compares; edit that file and copy it here. It is the
7
+ * transliteration of `@intutic/shared-types`'s phrases.ts, and both run the
8
+ * shared vectors.
9
+ */
10
+ export declare const PHRASES_PY_SOURCE: string;
11
+ //# sourceMappingURL=phrasesPy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phrasesPy.d.ts","sourceRoot":"","sources":["../../src/lib/phrasesPy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,QAoV7B,CAAA"}
@@ -0,0 +1,351 @@
1
+ /**
2
+ * The phrase matcher in Python, for the gates that run Python: the bash gates
3
+ * (their extractor already requires python3) and the Open WebUI filter.
4
+ *
5
+ * Byte-identical to `packages/intutic-clawde/intutic_clawde/gate/phrases.py`,
6
+ * which a test compares; edit that file and copy it here. It is the
7
+ * transliteration of `@intutic/shared-types`'s phrases.ts, and both run the
8
+ * shared vectors.
9
+ */
10
+ export const PHRASES_PY_SOURCE = String.raw `"""Phrase matching for shell commands and SQL in linear time.
11
+
12
+ A transliteration of packages/shared-types/src/phrases.ts; read that module's
13
+ docstring for what counts as a separator and why no regex searches the input.
14
+ The rules are the same; the walk is organised for CPython, which is slow per
15
+ step: runs of ordinary characters, spaces and backslashes are consumed by
16
+ anchored character-class matches (one pass each, nothing to backtrack into),
17
+ and a phrase is checked only at the words that can start it.
18
+
19
+ Every classifier runs the shared vectors in
20
+ packages/proxy/src/plugins/anomaly/action_vectors.json, and the hook gates
21
+ emit this file verbatim (services/sync-daemon/src/lib/phrasesPy.ts holds a
22
+ byte-identical copy, which a test compares). It must stay self-contained: the
23
+ standard library only, no module state beyond the constants below, and no
24
+ backtick or dollar-brace, which the TypeScript copy cannot hold.
25
+ """
26
+
27
+ import re
28
+ from bisect import bisect_left
29
+
30
+ _SPACE_CHARS = (
31
+ " \t\n\r\v\f\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007"
32
+ "\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
33
+ )
34
+ _SPACES = frozenset(_SPACE_CHARS)
35
+ _WORD = frozenset("abcdefghijklmnopqrstuvwxyz0123456789_")
36
+ # One anchored match per piece of the text. Every alternative starts with a
37
+ # character no other alternative can start with, so exactly one can match at
38
+ # a position and it matches one run of its class: nothing to backtrack into.
39
+ _PIECE = re.compile(
40
+ "(?P<plain>[^" + _SPACE_CHARS + "\\\\/;&|-]+)"
41
+ "|(?P<space>[" + _SPACE_CHARS + "]+)"
42
+ "|(?P<dash>-+)"
43
+ "|(?P<backslash>\\\\+)"
44
+ "|(?P<other>[/;&|])"
45
+ )
46
+ # A run of words that need no decision, each followed by one plain space:
47
+ # ordinary characters, after at most a leading run of dashes. Consumed in one
48
+ # match and split in C — most of a long command is this.
49
+ _BULK = re.compile("(?:(?:[^" + _SPACE_CHARS + "\\\\/;&|-]+|-+[^" + _SPACE_CHARS + "\\\\/;&|-]*) )+")
50
+ _BULK_NO_COMMENT = re.compile("(?:(?:[^" + _SPACE_CHARS + "\\\\;&|-]+|-+[^" + _SPACE_CHARS + "\\\\;&|-]*) )+")
51
+ # A run of line continuations or escaped spaces between words: one break.
52
+ _CONTINUATIONS = re.compile("(?:\\\\[" + _SPACE_CHARS + "])+")
53
+ # The same once no "*/" is left in the text: no "/*" can open a comment, so a
54
+ # slash is an ordinary character.
55
+ _PIECE_NO_COMMENT = re.compile(
56
+ "(?P<plain>[^" + _SPACE_CHARS + "\\\\;&|-]+)"
57
+ "|(?P<space>[" + _SPACE_CHARS + "]+)"
58
+ "|(?P<dash>-+)"
59
+ "|(?P<backslash>\\\\+)"
60
+ "|(?P<other>[;&|])"
61
+ )
62
+
63
+
64
+ class PhraseText:
65
+ """A text cut into words, ready for any number of has_phrase calls."""
66
+
67
+ __slots__ = ("raw", "joined", "toks", "sep", "nl", "eol", "dash", "bar", "tail", "memo")
68
+
69
+ def __init__(self):
70
+ self.raw = ""
71
+ self.joined = ""
72
+ self.toks = []
73
+ self.sep = []
74
+ self.nl = []
75
+ self.eol = []
76
+ self.dash = []
77
+ self.bar = []
78
+ self.tail = False
79
+ self.memo = {}
80
+
81
+
82
+ def phrase_text(value):
83
+ """Cut value into words in one pass (see phraseText in phrases.ts)."""
84
+ s = ("" if value is None else str(value)).lower()
85
+ n = len(s)
86
+ t = PhraseText()
87
+ t.raw = s
88
+ toks, sep, nl, eol, dash, bar = t.toks, t.sep, t.nl, t.eol, t.dash, t.bar
89
+ start = -1
90
+ tok_bar = False
91
+ pend_sep = pend_nl = pend_eol = False
92
+ close = -2 if "*/" in s else -1 # the next "*/" at or after the last search; -1 once none is left
93
+ piece = _PIECE if close != -1 else _PIECE_NO_COMMENT
94
+ bulk = _BULK if close != -1 else _BULK_NO_COMMENT
95
+ i = 0
96
+ while i < n:
97
+ if start < 0:
98
+ m = bulk.match(s, i)
99
+ if m is not None:
100
+ words = m.group().split(" ")
101
+ words.pop()
102
+ rest = len(words) - 1
103
+ toks.extend(words)
104
+ sep.append(pend_sep)
105
+ sep.extend([True] * rest)
106
+ nl.append(pend_nl)
107
+ nl.extend([False] * rest)
108
+ eol.append(pend_eol)
109
+ eol.extend([False] * rest)
110
+ dash.extend([w.startswith("--") for w in words])
111
+ bar.extend([False] * (rest + 1))
112
+ pend_sep = True
113
+ pend_nl = pend_eol = False
114
+ i = m.end()
115
+ continue
116
+ m = _CONTINUATIONS.match(s, i)
117
+ if m is not None:
118
+ pend_sep = True
119
+ i = m.end()
120
+ continue
121
+ m = piece.match(s, i)
122
+ kind = m.lastgroup
123
+ j = m.end()
124
+ if kind == "plain":
125
+ if start < 0:
126
+ start = i
127
+ i = j
128
+ continue
129
+ word_break = False
130
+ newline = eol_here = False
131
+ nxt = j
132
+ if kind == "space":
133
+ newline = eol_here = "\n" in m.group()
134
+ word_break = True
135
+ elif kind == "dash":
136
+ # A "--" inside a word starts a new one, glued to it: it can open a
137
+ # comment or an option run but cannot stand for a plain gap.
138
+ if j - i > 1 and start >= 0:
139
+ toks.append(s[start:i])
140
+ sep.append(pend_sep)
141
+ nl.append(pend_nl)
142
+ eol.append(pend_eol)
143
+ dash.append(s.startswith("--", start))
144
+ bar.append(tok_bar)
145
+ start = -1
146
+ tok_bar = False
147
+ pend_sep = pend_nl = pend_eol = False
148
+ if start < 0:
149
+ start = i
150
+ i = j
151
+ continue
152
+ elif kind == "backslash":
153
+ # Every backslash but the last is followed by a backslash: a
154
+ # literal character. The last one decides.
155
+ last = j - 1
156
+ d = s[j] if j < n else ""
157
+ if d in ("n", "t", "r") or (d != "" and d in _SPACES):
158
+ if last > i and start < 0:
159
+ start = i
160
+ word_break = True
161
+ eol_here = d == "n"
162
+ nxt = j + 1
163
+ if last > i:
164
+ # the literal backslashes end the word before the escape
165
+ toks.append(s[start:last])
166
+ sep.append(pend_sep)
167
+ nl.append(pend_nl)
168
+ eol.append(pend_eol)
169
+ dash.append(s.startswith("--", start))
170
+ bar.append(tok_bar)
171
+ start = -1
172
+ tok_bar = False
173
+ pend_sep = pend_nl = pend_eol = False
174
+ else:
175
+ if start < 0:
176
+ start = i
177
+ i = j
178
+ continue
179
+ else:
180
+ c = s[i]
181
+ if c == "/" and j < n and s[j] == "*" and close != -1:
182
+ if close < i + 2:
183
+ close = s.find("*/", i + 2)
184
+ if close == -1:
185
+ piece = _PIECE_NO_COMMENT
186
+ bulk = _BULK_NO_COMMENT
187
+ if close != -1:
188
+ word_break = True
189
+ nxt = close + 2
190
+ if not word_break:
191
+ if start < 0:
192
+ start = i
193
+ if c != "/":
194
+ tok_bar = True
195
+ i = j
196
+ continue
197
+ if start >= 0:
198
+ toks.append(s[start:i])
199
+ sep.append(pend_sep)
200
+ nl.append(pend_nl)
201
+ eol.append(pend_eol)
202
+ dash.append(s.startswith("--", start))
203
+ bar.append(tok_bar)
204
+ start = -1
205
+ tok_bar = False
206
+ pend_sep = pend_nl = pend_eol = False
207
+ pend_sep = True
208
+ if newline:
209
+ pend_nl = True
210
+ if eol_here:
211
+ pend_eol = True
212
+ i = nxt
213
+ if start >= 0:
214
+ toks.append(s[start:n])
215
+ sep.append(pend_sep)
216
+ nl.append(pend_nl)
217
+ eol.append(pend_eol)
218
+ dash.append(s.startswith("--", start))
219
+ bar.append(tok_bar)
220
+ pend_sep = False
221
+ t.tail = pend_sep
222
+ t.joined = " ".join(toks)
223
+ return t
224
+
225
+
226
+ def _next_index(t):
227
+ """Per word x: the first word at or after x containing ;&|, with a real
228
+ newline before it, and with any newline before it. Built once per text."""
229
+ ix = t.memo.get("#next")
230
+ if ix is None:
231
+ T = len(t.toks)
232
+ nb = [T] * (T + 1)
233
+ nn = [T] * (T + 1)
234
+ ne = [T] * (T + 1)
235
+ b = n_ = e = T
236
+ bar, nl, eol = t.bar, t.nl, t.eol
237
+ for x in range(T - 1, -1, -1):
238
+ if bar[x]:
239
+ b = x
240
+ if nl[x]:
241
+ n_ = x
242
+ if eol[x]:
243
+ e = x
244
+ nb[x] = b
245
+ nn[x] = n_
246
+ ne[x] = e
247
+ ix = (nb, nn, ne)
248
+ t.memo["#next"] = ix
249
+ return ix
250
+
251
+
252
+ def has_phrase(t, needle, bounded=False):
253
+ """Whether t contains needle (see hasPhrase in phrases.ts)."""
254
+ words = needle.lower().split(" ")
255
+ if len(words) == 1:
256
+ return words[0] in t.raw
257
+ trailing_gap = words[-1] == ""
258
+ if trailing_gap:
259
+ words.pop()
260
+ for w in words:
261
+ if w == "" or w not in t.joined:
262
+ return False
263
+ toks, sep, dash, tail = t.toks, t.sep, t.dash, t.tail
264
+ T = len(toks)
265
+ k = len(words)
266
+
267
+ def positions(i):
268
+ """The words that word i of the phrase can be, by its position."""
269
+ w = words[i]
270
+ first = i == 0
271
+ last = i == k - 1 and not trailing_gap
272
+ key = ("f" if first else "") + ("l" if last else "") + ("b" if bounded else "") + ":" + w
273
+ hit = t.memo.get(key)
274
+ if hit is not None:
275
+ return hit
276
+ lw = len(w)
277
+ if first:
278
+ out = [q for q, tok in enumerate(toks) if tok.endswith(w)]
279
+ elif last:
280
+ out = [q for q, tok in enumerate(toks) if tok.startswith(w)]
281
+ else:
282
+ out = [q for q, tok in enumerate(toks) if tok == w]
283
+ if bounded:
284
+ kept = []
285
+ for q in out:
286
+ tok = toks[q]
287
+ if first and len(tok) > lw and tok[len(tok) - lw - 1] in _WORD:
288
+ continue
289
+ if last and len(tok) > lw and (tok[lw] in _WORD or tok[lw] == "."):
290
+ continue
291
+ kept.append(q)
292
+ out = kept
293
+ t.memo[key] = out
294
+ return out
295
+
296
+ # The words at which the rest of the phrase matches, ascending.
297
+ ok = positions(k - 1)
298
+ if trailing_gap:
299
+ kept = []
300
+ for q in ok:
301
+ r = q + 1
302
+ if r < T:
303
+ if sep[r] or (dash[r] and not t.bar[r] and (r + 1 < T or tail)):
304
+ kept.append(q)
305
+ elif tail:
306
+ kept.append(q)
307
+ ok = kept
308
+ if k == 1:
309
+ return bool(ok)
310
+ nb, nn, ne = _next_index(t)
311
+ for i in range(k - 2, -1, -1):
312
+ ok_sep = [q for q in ok if sep[q]]
313
+ ok_set = set(ok_sep)
314
+ memo = {}
315
+
316
+ def option(r):
317
+ # An option run reaches any word before a ;&| word or a real newline.
318
+ j = bisect_left(ok_sep, r + 1)
319
+ return j < len(ok_sep) and ok_sep[j] <= min(nb[r], nn[r + 1], T - 1)
320
+
321
+ def reach(r):
322
+ # The gap that starts at word r leads into the rest of the phrase:
323
+ # directly, after an option run, or after -- comments, each of
324
+ # which runs to the next newline.
325
+ path = []
326
+ cur = r
327
+ while True:
328
+ if cur >= T:
329
+ val = False
330
+ break
331
+ if cur in memo:
332
+ val = memo[cur]
333
+ break
334
+ if cur in ok_set or (dash[cur] and option(cur)):
335
+ val = memo[cur] = True
336
+ break
337
+ if not dash[cur]:
338
+ val = memo[cur] = False
339
+ break
340
+ path.append(cur)
341
+ cur = ne[cur + 1]
342
+ for x in path:
343
+ memo[x] = val
344
+ return val
345
+
346
+ ok = [p for p in positions(i) if reach(p + 1)]
347
+ if not ok:
348
+ return False
349
+ return bool(ok)
350
+ `;
351
+ //# sourceMappingURL=phrasesPy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phrasesPy.js","sourceRoot":"","sources":["../../src/lib/phrasesPy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoV1C,CAAA"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * # Why
6
6
  *
7
- * TD-307: `POST /api/v1/hook-gate` was called by two of thirteen gates, so DLP,
7
+ * `POST /api/v1/hook-gate` was called by two of thirteen gates, so DLP,
8
8
  * `BLOCK:` SOPs, SSO policy and promoted findings reached two of thirteen. The
9
9
  * obvious repair — route the other eleven through the gate — is the wrong one:
10
10
  * those eleven enforce locally and **fail closed**, the two network callers were
@@ -54,13 +54,28 @@
54
54
  *
55
55
  * @module
56
56
  */
57
- import { type SsoGroupPolicy } from '@intutic/shared-types';
57
+ import { type McpRegistryRecord, type SsoGroupPolicy } from '@intutic/shared-types';
58
58
  import { type GuardPattern } from '../harness/protectedPaths.js';
59
59
  /** Directory every gate looks in. Inside `.intutic/hooks`, which is already
60
60
  * protected — see the module note. */
61
61
  export declare const DEFAULT_SNAPSHOT_DIR: string;
62
62
  export declare const SNAPSHOT_JSON = "policy-snapshot.json";
63
63
  export declare const SNAPSHOT_RULES = "policy-snapshot.rules";
64
+ /**
65
+ * The subdirectory of the snapshot directory holding the last snapshot the
66
+ * daemon wrote and so verified: both artifacts, byte for byte, read-only.
67
+ * Inside `.intutic/hooks`, which every gate refuses to touch. The settings
68
+ * guard compares the live snapshot with it and restores it when they differ
69
+ * ({@link restoreVerifiedSnapshot}), the way a policy agent keeps its last
70
+ * verified bundle active when a new one fails verification.
71
+ */
72
+ export declare const VERIFIED_SNAPSHOT_DIR = "verified";
73
+ /**
74
+ * Whether a `.rules` text verifies: it carries a `#digest` line, and the
75
+ * digest of its data lines matches it — the check every gate makes. A
76
+ * missing digest fails, since the writer always writes one.
77
+ */
78
+ export declare function snapshotRulesVerify(text: string): boolean;
64
79
  /**
65
80
  * Whether the destructive tier ships as `block` or as `warn`.
66
81
  *
@@ -94,10 +109,9 @@ export declare const DESTRUCTIVE_TIER_SEVERITY: 'block' | 'warn';
94
109
  * is not that kind of rule: it does not run `scanSkillContent` or judge
95
110
  * anything about what a skill file says, only WHERE a write's target path
96
111
  * points — `.agents/skills/**` or `.claude/skills/**`, a deterministic,
97
- * zero-ambiguity match. TD-358 (`docs/TECH_DEBT.md`) says this about exactly
98
- * that distinction: "Path-matching is the one exception, and it is not
112
+ * zero-ambiguity match. Path-matching is the one exception, and it is not
99
113
  * actually an exception to the measurement requirement — it never needed
100
- * one." There is no false-positive rate to earn a promotion by measuring,
114
+ * one. There is no false-positive rate to earn a promotion by measuring,
101
115
  * because there is nothing probabilistic here to measure in the first place.
102
116
  *
103
117
  * The second half of the argument is what `warn` actually buys on this
@@ -132,7 +146,7 @@ export declare const SKILL_SURFACE_TIER_SEVERITY: 'block' | 'warn';
132
146
  * ships as `block` or as `warn`.
133
147
  *
134
148
  * `block`, licensed by a measurement rather than by argument, unlike
135
- * `SKILL_SURFACE_TIER_SEVERITY` above. TD-358 held content enforcement at warn
149
+ * `SKILL_SURFACE_TIER_SEVERITY` above. Content enforcement was held at warn
136
150
  * until `scanSkillContent`'s false-positive rate on real benign skills had been
137
151
  * measured. It now has: 350 vendored SKILL.md files
138
152
  * (`packages/shared-types/src/__tests__/corpus/skills/`), and the nine patterns
@@ -160,7 +174,7 @@ export interface ResolvedPolicy {
160
174
  action: string;
161
175
  reason: string;
162
176
  /**
163
- * LLD #71: `'guardrail'` on rules the control plane projects from a cited
177
+ * `'guardrail'` on rules the control plane projects from a cited
164
178
  * policy guardrail. The only origin for which `action: 'warn'` is shipped
165
179
  * (at severity `warn`, report-only); every other warn rule is still
166
180
  * dropped, the HIGH/CRITICAL catch-all included.
@@ -175,9 +189,9 @@ export interface ResolvedPolicy {
175
189
  * this same field name off `workspaces.settings`, so this is not a new
176
190
  * name invented for the snapshot). Absent/empty means unrestricted — the
177
191
  * MCP proxy already reads it that way (`packages/mcp-proxy/src/policy.ts`),
178
- * and the gate-side `#mcpservers` header this field feeds
192
+ * and the gate-side `@mcp_allowlist` record this field feeds
179
193
  * (`writePolicySnapshot` below) preserves the same convention: an empty
180
- * list omits the header entirely rather than shipping a deny-everything one.
194
+ * list writes no record rather than a deny-everything one.
181
195
  */
182
196
  mcpAllowedServers: string[];
183
197
  /**
@@ -203,6 +217,14 @@ export interface ResolvedPolicy {
203
217
  memberId: string;
204
218
  ssoGroups: string[];
205
219
  } | null;
220
+ /**
221
+ * The workspace's MCP server registry decisions (`mcpRegistry`): blocked,
222
+ * held and approved servers, disabled tools and `mcpDefaultPolicy`. Written
223
+ * as an `@mcp_registry` record so the hook gates apply them to every
224
+ * `mcp__<server>__<tool>` call, not only the calls an MCP proxy fronts.
225
+ * Absent or null: an older control plane, and no record is written.
226
+ */
227
+ mcpRegistry?: McpRegistryRecord | null;
206
228
  }
207
229
  export interface PolicySnapshotOptions {
208
230
  controlPlaneUrl: string;
@@ -214,7 +236,7 @@ export interface PolicySnapshotOptions {
214
236
  * `review_before:` tokens from the workspace's local SOPs and settings
215
237
  * (`parseSopConstraints(...).reviewBefore`). Each becomes a `hold` rule,
216
238
  * `sop.local.review_before.<token>`, so every gate holds on them — not only
217
- * the Claude Code hook that used to bake them in (TD-474 item 4).
239
+ * the Claude Code hook that used to bake them in.
218
240
  */
219
241
  localHoldTokens?: readonly string[];
220
242
  }
@@ -244,6 +266,20 @@ export declare function writePolicySnapshot(policy: ResolvedPolicy, snapshotDir?
244
266
  digest: string;
245
267
  ruleCount: number;
246
268
  }>;
269
+ /**
270
+ * Puts the last verified snapshot back when the live one differs from it.
271
+ *
272
+ * `'intact'`: the live snapshot is the verified one, or there is nothing to
273
+ * compare (no snapshot written yet, or a live one from before the verified
274
+ * copy existed that still verifies). `'restored'`: either live artifact was
275
+ * edited, replaced or deleted, and both are the verified copy again.
276
+ * `'unverifiable'`: the live `.rules` fails its digest and there is no
277
+ * verified copy to restore, so the caller fetches a fresh snapshot. Any
278
+ * difference counts, a valid-looking one included: the daemon is the only
279
+ * writer of these files, so an older snapshot copied back in is tampering
280
+ * too. Never throws.
281
+ */
282
+ export declare function restoreVerifiedSnapshot(snapshotDir?: string): Promise<'intact' | 'restored' | 'unverifiable'>;
247
283
  /**
248
284
  * Fetch and write in one call. Safe to run on every sync cycle.
249
285
  *